• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
56 import java.io.ByteArrayOutputStream;
57 import java.io.File;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.OutputStream;
62 import java.io.UnsupportedEncodingException;
63 import java.util.ArrayList;
64 import java.util.HashSet;
65 import java.util.Map;
66 
67 /**
68  * This class is the high-level manager of PDU storage.
69  */
70 public class PduPersister {
71     private static final String TAG = "PduPersister";
72     private static final boolean LOCAL_LOGV = false;
73 
74     /**
75      * The uri of temporary drm objects.
76      */
77     public static final String TEMPORARY_DRM_OBJECT_URI =
78             "content://mms/" + Long.MAX_VALUE + "/part";
79 
80     /**
81      * Indicate that we transiently failed to process a MM.
82      */
83     public static final int PROC_STATUS_TRANSIENT_FAILURE = 1;
84 
85     /**
86      * Indicate that we permanently failed to process a MM.
87      */
88     public static final int PROC_STATUS_PERMANENTLY_FAILURE = 2;
89 
90     /**
91      * Indicate that we have successfully processed a MM.
92      */
93     public static final int PROC_STATUS_COMPLETED = 3;
94 
95     public static final String BEGIN_VCARD = "BEGIN:VCARD";
96 
97     private static PduPersister sPersister;
98 
99     private static final PduCache PDU_CACHE_INSTANCE;
100 
101     private static final int[] ADDRESS_FIELDS = new int[]{
102             PduHeaders.BCC,
103             PduHeaders.CC,
104             PduHeaders.FROM,
105             PduHeaders.TO
106     };
107 
108     public static final String[] PDU_PROJECTION = new String[]{
109             Mms._ID,
110             Mms.MESSAGE_BOX,
111             Mms.THREAD_ID,
112             Mms.RETRIEVE_TEXT,
113             Mms.SUBJECT,
114             Mms.CONTENT_LOCATION,
115             Mms.CONTENT_TYPE,
116             Mms.MESSAGE_CLASS,
117             Mms.MESSAGE_ID,
118             Mms.RESPONSE_TEXT,
119             Mms.TRANSACTION_ID,
120             Mms.CONTENT_CLASS,
121             Mms.DELIVERY_REPORT,
122             Mms.MESSAGE_TYPE,
123             Mms.MMS_VERSION,
124             Mms.PRIORITY,
125             Mms.READ_REPORT,
126             Mms.READ_STATUS,
127             Mms.REPORT_ALLOWED,
128             Mms.RETRIEVE_STATUS,
129             Mms.STATUS,
130             Mms.DATE,
131             Mms.DELIVERY_TIME,
132             Mms.EXPIRY,
133             Mms.MESSAGE_SIZE,
134             Mms.SUBJECT_CHARSET,
135             Mms.RETRIEVE_TEXT_CHARSET,
136             Mms.READ,
137             Mms.SEEN,
138     };
139 
140     public static final int PDU_COLUMN_ID                    = 0;
141     public static final int PDU_COLUMN_MESSAGE_BOX           = 1;
142     public static final int PDU_COLUMN_THREAD_ID             = 2;
143     public static final int PDU_COLUMN_RETRIEVE_TEXT         = 3;
144     public static final int PDU_COLUMN_SUBJECT               = 4;
145     public static final int PDU_COLUMN_CONTENT_LOCATION      = 5;
146     public static final int PDU_COLUMN_CONTENT_TYPE          = 6;
147     public static final int PDU_COLUMN_MESSAGE_CLASS         = 7;
148     public static final int PDU_COLUMN_MESSAGE_ID            = 8;
149     public static final int PDU_COLUMN_RESPONSE_TEXT         = 9;
150     public static final int PDU_COLUMN_TRANSACTION_ID        = 10;
151     public static final int PDU_COLUMN_CONTENT_CLASS         = 11;
152     public static final int PDU_COLUMN_DELIVERY_REPORT       = 12;
153     public static final int PDU_COLUMN_MESSAGE_TYPE          = 13;
154     public static final int PDU_COLUMN_MMS_VERSION           = 14;
155     public static final int PDU_COLUMN_PRIORITY              = 15;
156     public static final int PDU_COLUMN_READ_REPORT           = 16;
157     public static final int PDU_COLUMN_READ_STATUS           = 17;
158     public static final int PDU_COLUMN_REPORT_ALLOWED        = 18;
159     public static final int PDU_COLUMN_RETRIEVE_STATUS       = 19;
160     public static final int PDU_COLUMN_STATUS                = 20;
161     public static final int PDU_COLUMN_DATE                  = 21;
162     public static final int PDU_COLUMN_DELIVERY_TIME         = 22;
163     public static final int PDU_COLUMN_EXPIRY                = 23;
164     public static final int PDU_COLUMN_MESSAGE_SIZE          = 24;
165     public static final int PDU_COLUMN_SUBJECT_CHARSET       = 25;
166     public static final int PDU_COLUMN_RETRIEVE_TEXT_CHARSET = 26;
167     public static final int PDU_COLUMN_READ                  = 27;
168     public static final int PDU_COLUMN_SEEN                  = 28;
169 
170     private static final String[] PART_PROJECTION = new String[] {
171             Part._ID,
172             Part.CHARSET,
173             Part.CONTENT_DISPOSITION,
174             Part.CONTENT_ID,
175             Part.CONTENT_LOCATION,
176             Part.CONTENT_TYPE,
177             Part.FILENAME,
178             Part.NAME,
179             Part.TEXT
180     };
181 
182     private static final int PART_COLUMN_ID                  = 0;
183     private static final int PART_COLUMN_CHARSET             = 1;
184     private static final int PART_COLUMN_CONTENT_DISPOSITION = 2;
185     private static final int PART_COLUMN_CONTENT_ID          = 3;
186     private static final int PART_COLUMN_CONTENT_LOCATION    = 4;
187     private static final int PART_COLUMN_CONTENT_TYPE        = 5;
188     private static final int PART_COLUMN_FILENAME            = 6;
189     private static final int PART_COLUMN_NAME                = 7;
190     private static final int PART_COLUMN_TEXT                = 8;
191 
192     private static final SimpleArrayMap<Uri, Integer> MESSAGE_BOX_MAP;
193 
194     // These map are used for convenience in persist() and load().
195     private static final SparseIntArray CHARSET_COLUMN_INDEX_MAP;
196 
197     private static final SparseIntArray ENCODED_STRING_COLUMN_INDEX_MAP;
198 
199     private static final SparseIntArray TEXT_STRING_COLUMN_INDEX_MAP;
200 
201     private static final SparseIntArray OCTET_COLUMN_INDEX_MAP;
202 
203     private static final SparseIntArray LONG_COLUMN_INDEX_MAP;
204 
205     private static final SparseArray<String> CHARSET_COLUMN_NAME_MAP;
206 
207     private static final SparseArray<String> ENCODED_STRING_COLUMN_NAME_MAP;
208 
209     private static final SparseArray<String> TEXT_STRING_COLUMN_NAME_MAP;
210 
211     private static final SparseArray<String> OCTET_COLUMN_NAME_MAP;
212 
213     private static final SparseArray<String> LONG_COLUMN_NAME_MAP;
214 
215     static {
216         MESSAGE_BOX_MAP = new SimpleArrayMap<Uri, Integer>();
MESSAGE_BOX_MAP.put(Mms.Inbox.CONTENT_URI, Mms.MESSAGE_BOX_INBOX)217         MESSAGE_BOX_MAP.put(Mms.Inbox.CONTENT_URI, Mms.MESSAGE_BOX_INBOX);
MESSAGE_BOX_MAP.put(Mms.Sent.CONTENT_URI, Mms.MESSAGE_BOX_SENT)218         MESSAGE_BOX_MAP.put(Mms.Sent.CONTENT_URI, Mms.MESSAGE_BOX_SENT);
MESSAGE_BOX_MAP.put(Mms.Draft.CONTENT_URI, Mms.MESSAGE_BOX_DRAFTS)219         MESSAGE_BOX_MAP.put(Mms.Draft.CONTENT_URI, Mms.MESSAGE_BOX_DRAFTS);
MESSAGE_BOX_MAP.put(Mms.Outbox.CONTENT_URI, Mms.MESSAGE_BOX_OUTBOX)220         MESSAGE_BOX_MAP.put(Mms.Outbox.CONTENT_URI, Mms.MESSAGE_BOX_OUTBOX);
221 
222         CHARSET_COLUMN_INDEX_MAP = new SparseIntArray();
CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT_CHARSET)223         CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT_CHARSET);
CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT_CHARSET)224         CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT_CHARSET);
225 
226         CHARSET_COLUMN_NAME_MAP = new SparseArray<String>();
CHARSET_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT_CHARSET)227         CHARSET_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT_CHARSET);
CHARSET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT_CHARSET)228         CHARSET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT_CHARSET);
229 
230         // Encoded string field code -> column index/name map.
231         ENCODED_STRING_COLUMN_INDEX_MAP = new SparseIntArray();
ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT)232         ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT);
ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT)233         ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT);
234 
235         ENCODED_STRING_COLUMN_NAME_MAP = new SparseArray<String>();
ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT)236         ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT);
ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT)237         ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT);
238 
239         // Text string field code -> column index/name map.
240         TEXT_STRING_COLUMN_INDEX_MAP = new SparseIntArray();
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_LOCATION, PDU_COLUMN_CONTENT_LOCATION)241         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)242         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)243         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)244         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)245         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)246         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.TRANSACTION_ID, PDU_COLUMN_TRANSACTION_ID);
247 
248         TEXT_STRING_COLUMN_NAME_MAP = new SparseArray<String>();
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_LOCATION, Mms.CONTENT_LOCATION)249         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_LOCATION, Mms.CONTENT_LOCATION);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_TYPE, Mms.CONTENT_TYPE)250         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_TYPE, Mms.CONTENT_TYPE);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_CLASS, Mms.MESSAGE_CLASS)251         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_CLASS, Mms.MESSAGE_CLASS);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_ID, Mms.MESSAGE_ID)252         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_ID, Mms.MESSAGE_ID);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.RESPONSE_TEXT, Mms.RESPONSE_TEXT)253         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.RESPONSE_TEXT, Mms.RESPONSE_TEXT);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.TRANSACTION_ID, Mms.TRANSACTION_ID)254         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.TRANSACTION_ID, Mms.TRANSACTION_ID);
255 
256         // Octet field code -> column index/name map.
257         OCTET_COLUMN_INDEX_MAP = new SparseIntArray();
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_CLASS, PDU_COLUMN_CONTENT_CLASS)258         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_CLASS, PDU_COLUMN_CONTENT_CLASS);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_REPORT, PDU_COLUMN_DELIVERY_REPORT)259         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_REPORT, PDU_COLUMN_DELIVERY_REPORT);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_TYPE, PDU_COLUMN_MESSAGE_TYPE)260         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_TYPE, PDU_COLUMN_MESSAGE_TYPE);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MMS_VERSION, PDU_COLUMN_MMS_VERSION)261         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MMS_VERSION, PDU_COLUMN_MMS_VERSION);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.PRIORITY, PDU_COLUMN_PRIORITY)262         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.PRIORITY, PDU_COLUMN_PRIORITY);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_REPORT, PDU_COLUMN_READ_REPORT)263         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_REPORT, PDU_COLUMN_READ_REPORT);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_STATUS, PDU_COLUMN_READ_STATUS)264         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_STATUS, PDU_COLUMN_READ_STATUS);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.REPORT_ALLOWED, PDU_COLUMN_REPORT_ALLOWED)265         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.REPORT_ALLOWED, PDU_COLUMN_REPORT_ALLOWED);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_STATUS, PDU_COLUMN_RETRIEVE_STATUS)266         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_STATUS, PDU_COLUMN_RETRIEVE_STATUS);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.STATUS, PDU_COLUMN_STATUS)267         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.STATUS, PDU_COLUMN_STATUS);
268 
269         OCTET_COLUMN_NAME_MAP = new SparseArray<String>();
OCTET_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_CLASS, Mms.CONTENT_CLASS)270         OCTET_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_CLASS, Mms.CONTENT_CLASS);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_REPORT, Mms.DELIVERY_REPORT)271         OCTET_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_REPORT, Mms.DELIVERY_REPORT);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_TYPE, Mms.MESSAGE_TYPE)272         OCTET_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_TYPE, Mms.MESSAGE_TYPE);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.MMS_VERSION, Mms.MMS_VERSION)273         OCTET_COLUMN_NAME_MAP.put(PduHeaders.MMS_VERSION, Mms.MMS_VERSION);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.PRIORITY, Mms.PRIORITY)274         OCTET_COLUMN_NAME_MAP.put(PduHeaders.PRIORITY, Mms.PRIORITY);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_REPORT, Mms.READ_REPORT)275         OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_REPORT, Mms.READ_REPORT);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_STATUS, Mms.READ_STATUS)276         OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_STATUS, Mms.READ_STATUS);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.REPORT_ALLOWED, Mms.REPORT_ALLOWED)277         OCTET_COLUMN_NAME_MAP.put(PduHeaders.REPORT_ALLOWED, Mms.REPORT_ALLOWED);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_STATUS, Mms.RETRIEVE_STATUS)278         OCTET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_STATUS, Mms.RETRIEVE_STATUS);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.STATUS, Mms.STATUS)279         OCTET_COLUMN_NAME_MAP.put(PduHeaders.STATUS, Mms.STATUS);
280 
281         // Long field code -> column index/name map.
282         LONG_COLUMN_INDEX_MAP = new SparseIntArray();
LONG_COLUMN_INDEX_MAP.put(PduHeaders.DATE, PDU_COLUMN_DATE)283         LONG_COLUMN_INDEX_MAP.put(PduHeaders.DATE, PDU_COLUMN_DATE);
LONG_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_TIME, PDU_COLUMN_DELIVERY_TIME)284         LONG_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_TIME, PDU_COLUMN_DELIVERY_TIME);
LONG_COLUMN_INDEX_MAP.put(PduHeaders.EXPIRY, PDU_COLUMN_EXPIRY)285         LONG_COLUMN_INDEX_MAP.put(PduHeaders.EXPIRY, PDU_COLUMN_EXPIRY);
LONG_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_SIZE, PDU_COLUMN_MESSAGE_SIZE)286         LONG_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_SIZE, PDU_COLUMN_MESSAGE_SIZE);
287 
288         LONG_COLUMN_NAME_MAP = new SparseArray<String>();
LONG_COLUMN_NAME_MAP.put(PduHeaders.DATE, Mms.DATE)289         LONG_COLUMN_NAME_MAP.put(PduHeaders.DATE, Mms.DATE);
LONG_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_TIME, Mms.DELIVERY_TIME)290         LONG_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_TIME, Mms.DELIVERY_TIME);
LONG_COLUMN_NAME_MAP.put(PduHeaders.EXPIRY, Mms.EXPIRY)291         LONG_COLUMN_NAME_MAP.put(PduHeaders.EXPIRY, Mms.EXPIRY);
LONG_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_SIZE, Mms.MESSAGE_SIZE)292         LONG_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_SIZE, Mms.MESSAGE_SIZE);
293 
294         PDU_CACHE_INSTANCE = PduCache.getInstance();
295     }
296 
297     private final Context mContext;
298 
299     private final ContentResolver mContentResolver;
300 
PduPersister(final Context context)301     private PduPersister(final Context context) {
302         mContext = context;
303         mContentResolver = context.getContentResolver();
304     }
305 
306     /** Get(or create if not exist) an instance of PduPersister */
getPduPersister(final Context context)307     public static PduPersister getPduPersister(final Context context) {
308         if ((sPersister == null) || !context.equals(sPersister.mContext)) {
309             sPersister = new PduPersister(context);
310         }
311         if (LOCAL_LOGV) {
312             LogUtil.v(TAG, "PduPersister getPduPersister");
313         }
314 
315         return sPersister;
316     }
317 
setEncodedStringValueToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)318     private void setEncodedStringValueToHeaders(
319             final Cursor c, final int columnIndex,
320             final PduHeaders headers, final int mapColumn) {
321         final String s = c.getString(columnIndex);
322         if ((s != null) && (s.length() > 0)) {
323             final int charsetColumnIndex = CHARSET_COLUMN_INDEX_MAP.get(mapColumn);
324             final int charset = c.getInt(charsetColumnIndex);
325             final EncodedStringValue value = new EncodedStringValue(
326                     charset, getBytes(s));
327             headers.setEncodedStringValue(value, mapColumn);
328         }
329     }
330 
setTextStringToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)331     private void setTextStringToHeaders(
332             final Cursor c, final int columnIndex,
333             final PduHeaders headers, final int mapColumn) {
334         final String s = c.getString(columnIndex);
335         if (s != null) {
336             headers.setTextString(getBytes(s), mapColumn);
337         }
338     }
339 
setOctetToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)340     private void setOctetToHeaders(
341             final Cursor c, final int columnIndex,
342             final PduHeaders headers, final int mapColumn) throws InvalidHeaderValueException {
343         if (!c.isNull(columnIndex)) {
344             final int b = c.getInt(columnIndex);
345             headers.setOctet(b, mapColumn);
346         }
347     }
348 
setLongToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)349     private void setLongToHeaders(
350             final Cursor c, final int columnIndex,
351             final PduHeaders headers, final int mapColumn) {
352         if (!c.isNull(columnIndex)) {
353             final long l = c.getLong(columnIndex);
354             headers.setLongInteger(l, mapColumn);
355         }
356     }
357 
getIntegerFromPartColumn(final Cursor c, final int columnIndex)358     private Integer getIntegerFromPartColumn(final Cursor c, final int columnIndex) {
359         if (!c.isNull(columnIndex)) {
360             return c.getInt(columnIndex);
361         }
362         return null;
363     }
364 
getByteArrayFromPartColumn(final Cursor c, final int columnIndex)365     private byte[] getByteArrayFromPartColumn(final Cursor c, final int columnIndex) {
366         if (!c.isNull(columnIndex)) {
367             return getBytes(c.getString(columnIndex));
368         }
369         return null;
370     }
371 
loadParts(final long msgId)372     private PduPart[] loadParts(final long msgId) throws MmsException {
373         final Cursor c = SqliteWrapper.query(mContext, mContentResolver,
374                 Uri.parse("content://mms/" + msgId + "/part"),
375                 PART_PROJECTION, null, null, null);
376 
377         PduPart[] parts = null;
378 
379         try {
380             if ((c == null) || (c.getCount() == 0)) {
381                 if (LOCAL_LOGV) {
382                     LogUtil.v(TAG, "loadParts(" + msgId + "): no part to load.");
383                 }
384                 return null;
385             }
386 
387             final int partCount = c.getCount();
388             int partIdx = 0;
389             parts = new PduPart[partCount];
390             while (c.moveToNext()) {
391                 final PduPart part = new PduPart();
392                 final Integer charset = getIntegerFromPartColumn(
393                         c, PART_COLUMN_CHARSET);
394                 if (charset != null) {
395                     part.setCharset(charset);
396                 }
397 
398                 final byte[] contentDisposition = getByteArrayFromPartColumn(
399                         c, PART_COLUMN_CONTENT_DISPOSITION);
400                 if (contentDisposition != null) {
401                     part.setContentDisposition(contentDisposition);
402                 }
403 
404                 final byte[] contentId = getByteArrayFromPartColumn(
405                         c, PART_COLUMN_CONTENT_ID);
406                 if (contentId != null) {
407                     part.setContentId(contentId);
408                 }
409 
410                 final byte[] contentLocation = getByteArrayFromPartColumn(
411                         c, PART_COLUMN_CONTENT_LOCATION);
412                 if (contentLocation != null) {
413                     part.setContentLocation(contentLocation);
414                 }
415 
416                 final byte[] contentType = getByteArrayFromPartColumn(
417                         c, PART_COLUMN_CONTENT_TYPE);
418                 if (contentType != null) {
419                     part.setContentType(contentType);
420                 } else {
421                     throw new MmsException("Content-Type must be set.");
422                 }
423 
424                 final byte[] fileName = getByteArrayFromPartColumn(
425                         c, PART_COLUMN_FILENAME);
426                 if (fileName != null) {
427                     part.setFilename(fileName);
428                 }
429 
430                 final byte[] name = getByteArrayFromPartColumn(
431                         c, PART_COLUMN_NAME);
432                 if (name != null) {
433                     part.setName(name);
434                 }
435 
436                 // Construct a Uri for this part.
437                 final long partId = c.getLong(PART_COLUMN_ID);
438                 final Uri partURI = Uri.parse("content://mms/part/" + partId);
439                 part.setDataUri(partURI);
440 
441                 // For images/audio/video, we won't keep their data in Part
442                 // because their renderer accept Uri as source.
443                 final String type = toIsoString(contentType);
444                 if (!ContentType.isImageType(type)
445                         && !ContentType.isAudioType(type)
446                         && !ContentType.isVideoType(type)) {
447                     final ByteArrayOutputStream baos = new ByteArrayOutputStream();
448                     InputStream is = null;
449 
450                     // Store simple string values directly in the database instead of an
451                     // external file.  This makes the text searchable and retrieval slightly
452                     // faster.
453                     if (ContentType.TEXT_PLAIN.equals(type) || ContentType.APP_SMIL.equals(type)
454                             || ContentType.TEXT_HTML.equals(type)) {
455                         final String text = c.getString(PART_COLUMN_TEXT);
456                         final byte[] blob = new EncodedStringValue(
457                                 charset != null ? charset : CharacterSets.DEFAULT_CHARSET,
458                                 text != null ? text : "")
459                                 .getTextString();
460                         baos.write(blob, 0, blob.length);
461                     } else {
462 
463                         try {
464                             is = mContentResolver.openInputStream(partURI);
465 
466                             final byte[] buffer = new byte[256];
467                             int len = is.read(buffer);
468                             while (len >= 0) {
469                                 baos.write(buffer, 0, len);
470                                 len = is.read(buffer);
471                             }
472                         } catch (final IOException e) {
473                             Log.e(TAG, "Failed to load part data", e);
474                             c.close();
475                             throw new MmsException(e);
476                         } finally {
477                             if (is != null) {
478                                 try {
479                                     is.close();
480                                 } catch (final IOException e) {
481                                     Log.e(TAG, "Failed to close stream", e);
482                                 } // Ignore
483                             }
484                         }
485                     }
486                     part.setData(baos.toByteArray());
487                 }
488                 parts[partIdx++] = part;
489             }
490         } finally {
491             if (c != null) {
492                 c.close();
493             }
494         }
495 
496         return parts;
497     }
498 
loadAddress(final long msgId, final PduHeaders headers)499     private void loadAddress(final long msgId, final PduHeaders headers) {
500         final Cursor c = SqliteWrapper.query(mContext, mContentResolver,
501                 Uri.parse("content://mms/" + msgId + "/addr"),
502                 new String[]{Addr.ADDRESS, Addr.CHARSET, Addr.TYPE},
503                 null, null, null);
504 
505         if (c != null) {
506             try {
507                 while (c.moveToNext()) {
508                     final String addr = c.getString(0);
509                     if (!TextUtils.isEmpty(addr)) {
510                         final int addrType = c.getInt(2);
511                         switch (addrType) {
512                             case PduHeaders.FROM:
513                                 headers.setEncodedStringValue(
514                                         new EncodedStringValue(c.getInt(1), getBytes(addr)),
515                                         addrType);
516                                 break;
517                             case PduHeaders.TO:
518                             case PduHeaders.CC:
519                             case PduHeaders.BCC:
520                                 headers.appendEncodedStringValue(
521                                         new EncodedStringValue(c.getInt(1), getBytes(addr)),
522                                         addrType);
523                                 break;
524                             default:
525                                 Log.e(TAG, "Unknown address type: " + addrType);
526                                 break;
527                         }
528                     }
529                 }
530             } finally {
531                 c.close();
532             }
533         }
534     }
535 
536     /**
537      * Load a PDU from a given cursor
538      *
539      * @param c The cursor
540      * @return A parsed PDU from the database row
541      */
load(final Cursor c)542     public GenericPdu load(final Cursor c) throws MmsException {
543         final PduHeaders headers = new PduHeaders();
544         final long msgId = c.getLong(PDU_COLUMN_ID);
545         // Fill in the headers from the PDU columns
546         loadHeadersFromCursor(c, headers);
547         // Load address information of the MM.
548         loadAddress(msgId, headers);
549         // Load parts for the PDU body
550         final int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE);
551         final PduBody body = loadBody(msgId, msgType);
552         return createPdu(msgType, headers, body);
553     }
554 
555     /**
556      * Load a PDU from storage by given Uri.
557      *
558      * @param uri            The Uri of the PDU to be loaded.
559      * @return A generic PDU object, it may be cast to dedicated PDU.
560      * @throws MmsException Failed to load some fields of a PDU.
561      */
load(final Uri uri)562     public GenericPdu load(final Uri uri) throws MmsException {
563         GenericPdu pdu = null;
564         PduCacheEntry cacheEntry = null;
565         int msgBox = 0;
566         final long threadId = -1;
567         try {
568             synchronized (PDU_CACHE_INSTANCE) {
569                 if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
570                     if (LOCAL_LOGV) {
571                         LogUtil.v(TAG, "load: " + uri + " blocked by isUpdating()");
572                     }
573                     try {
574                         PDU_CACHE_INSTANCE.wait();
575                     } catch (final InterruptedException e) {
576                         Log.e(TAG, "load: ", e);
577                     }
578                 }
579 
580                 // Check if the pdu is already loaded
581                 cacheEntry = PDU_CACHE_INSTANCE.get(uri);
582                 if (cacheEntry != null) {
583                     return cacheEntry.getPdu();
584                 }
585 
586                 // Tell the cache to indicate to other callers that this item
587                 // is currently being updated.
588                 PDU_CACHE_INSTANCE.setUpdating(uri, true);
589             }
590 
591             final Cursor c = SqliteWrapper.query(mContext, mContentResolver, uri,
592                     PDU_PROJECTION, null, null, null);
593             final PduHeaders headers = new PduHeaders();
594             final long msgId = ContentUris.parseId(uri);
595 
596             try {
597                 if ((c == null) || (c.getCount() != 1) || !c.moveToFirst()) {
598                     return null;  // MMS not found
599                 }
600 
601                 msgBox = c.getInt(PDU_COLUMN_MESSAGE_BOX);
602                 //threadId = c.getLong(PDU_COLUMN_THREAD_ID);
603                 loadHeadersFromCursor(c, headers);
604             } finally {
605                 if (c != null) {
606                     c.close();
607                 }
608             }
609 
610             // Check whether 'msgId' has been assigned a valid value.
611             if (msgId == -1L) {
612                 throw new MmsException("Error! ID of the message: -1.");
613             }
614 
615             // Load address information of the MM.
616             loadAddress(msgId, headers);
617 
618             final int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE);
619             final PduBody body = loadBody(msgId, msgType);
620             pdu = createPdu(msgType, headers, body);
621         } finally {
622             synchronized (PDU_CACHE_INSTANCE) {
623                 if (pdu != null) {
624                     Assert.isNull(PDU_CACHE_INSTANCE.get(uri), "Pdu exists for " + uri);
625                     // Update the cache entry with the real info
626                     cacheEntry = new PduCacheEntry(pdu, msgBox, threadId);
627                     PDU_CACHE_INSTANCE.put(uri, cacheEntry);
628                 }
629                 PDU_CACHE_INSTANCE.setUpdating(uri, false);
630                 PDU_CACHE_INSTANCE.notifyAll(); // tell anybody waiting on this entry to go ahead
631             }
632         }
633         return pdu;
634     }
635 
loadHeadersFromCursor(final Cursor c, final PduHeaders headers)636     private void loadHeadersFromCursor(final Cursor c, final PduHeaders headers)
637             throws InvalidHeaderValueException {
638         for (int i = ENCODED_STRING_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
639             setEncodedStringValueToHeaders(
640                     c, ENCODED_STRING_COLUMN_INDEX_MAP.valueAt(i), headers,
641                     ENCODED_STRING_COLUMN_INDEX_MAP.keyAt(i));
642         }
643         for (int i = TEXT_STRING_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
644             setTextStringToHeaders(
645                     c, TEXT_STRING_COLUMN_INDEX_MAP.valueAt(i), headers,
646                     TEXT_STRING_COLUMN_INDEX_MAP.keyAt(i));
647         }
648         for (int i = OCTET_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
649             setOctetToHeaders(
650                     c, OCTET_COLUMN_INDEX_MAP.valueAt(i), headers,
651                     OCTET_COLUMN_INDEX_MAP.keyAt(i));
652         }
653         for (int i = LONG_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
654             setLongToHeaders(
655                     c, LONG_COLUMN_INDEX_MAP.valueAt(i), headers,
656                     LONG_COLUMN_INDEX_MAP.keyAt(i));
657         }
658     }
659 
createPdu(final int msgType, final PduHeaders headers, final PduBody body)660     private GenericPdu createPdu(final int msgType, final PduHeaders headers, final PduBody body)
661             throws MmsException {
662         switch (msgType) {
663             case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
664                 return new NotificationInd(headers);
665             case PduHeaders.MESSAGE_TYPE_DELIVERY_IND:
666                 return new DeliveryInd(headers);
667             case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND:
668                 return new ReadOrigInd(headers);
669             case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF:
670                 return new RetrieveConf(headers, body);
671             case PduHeaders.MESSAGE_TYPE_SEND_REQ:
672                 return new SendReq(headers, body);
673             case PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND:
674                 return new AcknowledgeInd(headers);
675             case PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND:
676                 return new NotifyRespInd(headers);
677             case PduHeaders.MESSAGE_TYPE_READ_REC_IND:
678                 return new ReadRecInd(headers);
679             case PduHeaders.MESSAGE_TYPE_SEND_CONF:
680             case PduHeaders.MESSAGE_TYPE_FORWARD_REQ:
681             case PduHeaders.MESSAGE_TYPE_FORWARD_CONF:
682             case PduHeaders.MESSAGE_TYPE_MBOX_STORE_REQ:
683             case PduHeaders.MESSAGE_TYPE_MBOX_STORE_CONF:
684             case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_REQ:
685             case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_CONF:
686             case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_REQ:
687             case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_CONF:
688             case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_REQ:
689             case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_CONF:
690             case PduHeaders.MESSAGE_TYPE_MBOX_DESCR:
691             case PduHeaders.MESSAGE_TYPE_DELETE_REQ:
692             case PduHeaders.MESSAGE_TYPE_DELETE_CONF:
693             case PduHeaders.MESSAGE_TYPE_CANCEL_REQ:
694             case PduHeaders.MESSAGE_TYPE_CANCEL_CONF:
695                 throw new MmsException(
696                         "Unsupported PDU type: " + Integer.toHexString(msgType));
697 
698             default:
699                 throw new MmsException(
700                         "Unrecognized PDU type: " + Integer.toHexString(msgType));
701         }
702     }
703 
loadBody(final long msgId, final int msgType)704     private PduBody loadBody(final long msgId, final int msgType) throws MmsException {
705         final PduBody body = new PduBody();
706 
707         // For PDU which type is M_retrieve.conf or Send.req, we should
708         // load multiparts and put them into the body of the PDU.
709         if ((msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)
710                 || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) {
711             final PduPart[] parts = loadParts(msgId);
712             if (parts != null) {
713                 final int partsNum = parts.length;
714                 for (int i = 0; i < partsNum; i++) {
715                     body.addPart(parts[i]);
716                 }
717             }
718         }
719 
720         return body;
721     }
722 
persistAddress( final long msgId, final int type, final EncodedStringValue[] array)723     private void persistAddress(
724             final long msgId, final int type, final EncodedStringValue[] array) {
725         final ContentValues values = new ContentValues(3);
726 
727         for (final EncodedStringValue addr : array) {
728             values.clear(); // Clear all values first.
729             values.put(Addr.ADDRESS, toIsoString(addr.getTextString()));
730             values.put(Addr.CHARSET, addr.getCharacterSet());
731             values.put(Addr.TYPE, type);
732 
733             final Uri uri = Uri.parse("content://mms/" + msgId + "/addr");
734             SqliteWrapper.insert(mContext, mContentResolver, uri, values);
735         }
736     }
737 
getPartContentType(final PduPart part)738     private static String getPartContentType(final PduPart part) {
739         return part.getContentType() == null ? null : toIsoString(part.getContentType());
740     }
741 
getValues(final PduPart part, final ContentValues values)742     private static void getValues(final PduPart part, final ContentValues values) {
743         byte[] bytes = part.getFilename();
744         if (bytes != null) {
745             values.put(Part.FILENAME, new String(bytes));
746         }
747 
748         bytes = part.getName();
749         if (bytes != null) {
750             values.put(Part.NAME, new String(bytes));
751         }
752 
753         bytes = part.getContentDisposition();
754         if (bytes != null) {
755             values.put(Part.CONTENT_DISPOSITION, toIsoString(bytes));
756         }
757 
758         bytes = part.getContentId();
759         if (bytes != null) {
760             values.put(Part.CONTENT_ID, toIsoString(bytes));
761         }
762 
763         bytes = part.getContentLocation();
764         if (bytes != null) {
765             values.put(Part.CONTENT_LOCATION, toIsoString(bytes));
766         }
767     }
768 
persistPart(final PduPart part, final long msgId, final Map<Uri, InputStream> preOpenedFiles)769     public Uri persistPart(final PduPart part, final long msgId,
770             final Map<Uri, InputStream> preOpenedFiles) throws MmsException {
771         final Uri uri = Uri.parse("content://mms/" + msgId + "/part");
772         final ContentValues values = new ContentValues(8);
773 
774         final int charset = part.getCharset();
775         if (charset != 0) {
776             values.put(Part.CHARSET, charset);
777         }
778 
779         String contentType = getPartContentType(part);
780         final byte[] data = part.getData();
781 
782         if (LOCAL_LOGV) {
783             LogUtil.v(TAG, "PduPersister.persistPart part: " + uri + " contentType: " +
784                     contentType);
785         }
786 
787         if (contentType != null) {
788             // There is no "image/jpg" in Android (and it's an invalid mimetype).
789             // Change it to "image/jpeg"
790             if (ContentType.IMAGE_JPG.equals(contentType)) {
791                 contentType = ContentType.IMAGE_JPEG;
792             }
793 
794             // On somes phones, a vcard comes in as text/plain instead of text/v-card.
795             // Fix it if necessary.
796             if (ContentType.TEXT_PLAIN.equals(contentType) && data != null) {
797                 // There might be a more efficient way to just check the beginning of the string
798                 // without encoding the whole thing, but we're concerned that with various
799                 // characters sets, just comparing the byte data to BEGIN_VCARD would not be
800                 // reliable.
801                 final String encodedDataString = new EncodedStringValue(charset, data).getString();
802                 if (encodedDataString != null && encodedDataString.startsWith(BEGIN_VCARD)) {
803                     contentType = ContentType.TEXT_VCARD;
804                     part.setContentType(contentType.getBytes());
805                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
806                         LogUtil.d(TAG, "PduPersister.persistPart part: " + uri + " contentType: " +
807                                 contentType + " changing to vcard");
808                     }
809                 }
810             }
811 
812             values.put(Part.CONTENT_TYPE, contentType);
813             // To ensure the SMIL part is always the first part.
814             if (ContentType.APP_SMIL.equals(contentType)) {
815                 values.put(Part.SEQ, -1);
816             }
817         } else {
818             throw new MmsException("MIME type of the part must be set.");
819         }
820 
821         getValues(part, values);
822 
823         Uri res = null;
824 
825         try {
826             res = SqliteWrapper.insert(mContext, mContentResolver, uri, values);
827         } catch (IllegalStateException e) {
828             // Currently the MMS provider throws an IllegalStateException when it's out of space
829             LogUtil.e(TAG, "SqliteWrapper.insert threw: ", e);
830         }
831 
832         if (res == null) {
833             throw new MmsException("Failed to persist part, return null.");
834         }
835 
836         persistData(part, res, contentType, preOpenedFiles);
837         // After successfully store the data, we should update
838         // the dataUri of the part.
839         part.setDataUri(res);
840 
841         return res;
842     }
843 
844     /**
845      * Save data of the part into storage. The source data may be given
846      * by a byte[] or a Uri. If it's a byte[], directly save it
847      * into storage, otherwise load source data from the dataUri and then
848      * save it. If the data is an image, we may scale down it according
849      * to user preference.
850      *
851      * @param part           The PDU part which contains data to be saved.
852      * @param uri            The URI of the part.
853      * @param contentType    The MIME type of the part.
854      * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts.
855      * @throws MmsException Cannot find source data or error occurred
856      *                      while saving the data.
857      */
persistData(final PduPart part, final Uri uri, final String contentType, final Map<Uri, InputStream> preOpenedFiles)858     private void persistData(final PduPart part, final Uri uri,
859             final String contentType, final Map<Uri, InputStream> preOpenedFiles)
860             throws MmsException {
861         OutputStream os = null;
862         InputStream is = null;
863         DrmConvertSession drmConvertSession = null;
864         Uri dataUri = null;
865         String path = null;
866 
867         try {
868             final byte[] data = part.getData();
869             final int charset = part.getCharset();
870             if (ContentType.TEXT_PLAIN.equals(contentType)
871                     || ContentType.APP_SMIL.equals(contentType)
872                     || ContentType.TEXT_HTML.equals(contentType)) {
873                 // Some phone could send MMS with a text part having empty data
874                 // Let's just skip those parts.
875                 // EncodedStringValue() throws NPE if data is empty
876                 if (data != null) {
877                     final ContentValues cv = new ContentValues();
878                     cv.put(Mms.Part.TEXT, new EncodedStringValue(charset, data).getString());
879                     if (mContentResolver.update(uri, cv, null, null) != 1) {
880                         throw new MmsException("unable to update " + uri.toString());
881                     }
882                 }
883             } else {
884                 final boolean isDrm = DownloadDrmHelper.isDrmConvertNeeded(contentType);
885                 if (isDrm) {
886                     if (uri != null) {
887                         try {
888                             path = convertUriToPath(mContext, uri);
889                             if (LOCAL_LOGV) {
890                                 LogUtil.v(TAG, "drm uri: " + uri + " path: " + path);
891                             }
892                             final File f = new File(path);
893                             final long len = f.length();
894                             if (LOCAL_LOGV) {
895                                 LogUtil.v(TAG, "drm path: " + path + " len: " + len);
896                             }
897                             if (len > 0) {
898                                 // we're not going to re-persist and re-encrypt an already
899                                 // converted drm file
900                                 return;
901                             }
902                         } catch (final Exception e) {
903                             Log.e(TAG, "Can't get file info for: " + part.getDataUri(), e);
904                         }
905                     }
906                     // We haven't converted the file yet, start the conversion
907                     drmConvertSession = DrmConvertSession.open(mContext, contentType);
908                     if (drmConvertSession == null) {
909                         throw new MmsException("Mimetype " + contentType +
910                                 " can not be converted.");
911                     }
912                 }
913                 // uri can look like:
914                 // content://mms/part/98
915                 os = mContentResolver.openOutputStream(uri);
916                 if (os == null) {
917                     throw new MmsException("Failed to create output stream on " + uri);
918                 }
919                 if (data == null) {
920                     dataUri = part.getDataUri();
921                     if ((dataUri == null) || (dataUri.equals(uri))) {
922                         Log.w(TAG, "Can't find data for this part.");
923                         return;
924                     }
925                     // dataUri can look like:
926                     // content://com.google.android.gallery3d.provider/picasa/item/5720646660183715
927                     if (preOpenedFiles != null && preOpenedFiles.containsKey(dataUri)) {
928                         is = preOpenedFiles.get(dataUri);
929                     }
930                     if (is == null) {
931                         is = mContentResolver.openInputStream(dataUri);
932                     }
933                     if (is == null) {
934                         throw new MmsException("Failed to create input stream on " + dataUri);
935                     }
936                     if (LOCAL_LOGV) {
937                         LogUtil.v(TAG, "Saving data to: " + uri);
938                     }
939 
940                     final byte[] buffer = new byte[8192];
941                     for (int len = 0; (len = is.read(buffer)) != -1; ) {
942                         if (!isDrm) {
943                             os.write(buffer, 0, len);
944                         } else {
945                             final byte[] convertedData = drmConvertSession.convert(buffer, len);
946                             if (convertedData != null) {
947                                 os.write(convertedData, 0, convertedData.length);
948                             } else {
949                                 throw new MmsException("Error converting drm data.");
950                             }
951                         }
952                     }
953                 } else {
954                     if (LOCAL_LOGV) {
955                         LogUtil.v(TAG, "Saving data to: " + uri);
956                     }
957                     if (!isDrm) {
958                         os.write(data);
959                     } else {
960                         dataUri = uri;
961                         final byte[] convertedData = drmConvertSession.convert(data, data.length);
962                         if (convertedData != null) {
963                             os.write(convertedData, 0, convertedData.length);
964                         } else {
965                             throw new MmsException("Error converting drm data.");
966                         }
967                     }
968                 }
969             }
970         } catch (final SQLiteException e) {
971             Log.e(TAG, "Failed with SQLiteException.", e);
972             throw new MmsException(e);
973         } catch (final FileNotFoundException e) {
974             Log.e(TAG, "Failed to open Input/Output stream.", e);
975             throw new MmsException(e);
976         } catch (final IOException e) {
977             Log.e(TAG, "Failed to read/write data.", e);
978             throw new MmsException(e);
979         } finally {
980             if (os != null) {
981                 try {
982                     os.close();
983                 } catch (final IOException e) {
984                     Log.e(TAG, "IOException while closing: " + os, e);
985                 } // Ignore
986             }
987             if (is != null) {
988                 try {
989                     is.close();
990                 } catch (final IOException e) {
991                     Log.e(TAG, "IOException while closing: " + is, e);
992                 } // Ignore
993             }
994             if (drmConvertSession != null) {
995                 drmConvertSession.close(path);
996 
997                 // Reset the permissions on the encrypted part file so everyone has only read
998                 // permission.
999                 final File f = new File(path);
1000                 final ContentValues values = new ContentValues(0);
1001                 SqliteWrapper.update(mContext, mContentResolver,
1002                         Uri.parse("content://mms/resetFilePerm/" + f.getName()),
1003                         values, null, null);
1004             }
1005         }
1006     }
1007 
1008     /**
1009      * This method expects uri in the following format
1010      *     content://media/<table_name>/<row_index> (or)
1011      *     file://sdcard/test.mp4
1012      *     http://test.com/test.mp4
1013      *
1014      * Here <table_name> shall be "video" or "audio" or "images"
1015      * <row_index> the index of the content in given table
1016      */
convertUriToPath(final Context context, final Uri uri)1017     public static String convertUriToPath(final Context context, final Uri uri) {
1018         String path = null;
1019         if (null != uri) {
1020             final String scheme = uri.getScheme();
1021             if (null == scheme || scheme.equals("") ||
1022                     scheme.equals(ContentResolver.SCHEME_FILE)) {
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