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
18package com.android.messaging.mmslib.pdu;
19
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.database.Cursor;
25import android.database.DatabaseUtils;
26import android.database.sqlite.SQLiteException;
27import android.net.Uri;
28import android.provider.MediaStore;
29import android.provider.Telephony.Mms;
30import android.provider.Telephony.Mms.Addr;
31import android.provider.Telephony.Mms.Part;
32import android.provider.Telephony.MmsSms;
33import android.provider.Telephony.MmsSms.PendingMessages;
34import android.support.v4.util.ArrayMap;
35import android.support.v4.util.SimpleArrayMap;
36import android.telephony.PhoneNumberUtils;
37import android.text.TextUtils;
38import android.util.Log;
39import android.util.SparseArray;
40import android.util.SparseIntArray;
41
42import com.android.messaging.datamodel.data.ParticipantData;
43import com.android.messaging.mmslib.InvalidHeaderValueException;
44import com.android.messaging.mmslib.MmsException;
45import com.android.messaging.mmslib.SqliteWrapper;
46import com.android.messaging.mmslib.util.DownloadDrmHelper;
47import com.android.messaging.mmslib.util.DrmConvertSession;
48import com.android.messaging.mmslib.util.PduCache;
49import com.android.messaging.mmslib.util.PduCacheEntry;
50import com.android.messaging.sms.MmsSmsUtils;
51import com.android.messaging.util.Assert;
52import com.android.messaging.util.ContentType;
53import com.android.messaging.util.LogUtil;
54import com.android.messaging.util.OsUtil;
55
56import java.io.ByteArrayOutputStream;
57import java.io.File;
58import java.io.FileNotFoundException;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.OutputStream;
62import java.io.UnsupportedEncodingException;
63import java.util.ArrayList;
64import java.util.HashSet;
65import java.util.Map;
66
67/**
68 * This class is the high-level manager of PDU storage.
69 */
70public 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>();
217        MESSAGE_BOX_MAP.put(Mms.Inbox.CONTENT_URI, Mms.MESSAGE_BOX_INBOX);
218        MESSAGE_BOX_MAP.put(Mms.Sent.CONTENT_URI, Mms.MESSAGE_BOX_SENT);
219        MESSAGE_BOX_MAP.put(Mms.Draft.CONTENT_URI, Mms.MESSAGE_BOX_DRAFTS);
220        MESSAGE_BOX_MAP.put(Mms.Outbox.CONTENT_URI, Mms.MESSAGE_BOX_OUTBOX);
221
222        CHARSET_COLUMN_INDEX_MAP = new SparseIntArray();
223        CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT_CHARSET);
224        CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT_CHARSET);
225
226        CHARSET_COLUMN_NAME_MAP = new SparseArray<String>();
227        CHARSET_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT_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();
232        ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT);
233        ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT);
234
235        ENCODED_STRING_COLUMN_NAME_MAP = new SparseArray<String>();
236        ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT);
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();
241        TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_LOCATION, PDU_COLUMN_CONTENT_LOCATION);
242        TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_TYPE, PDU_COLUMN_CONTENT_TYPE);
243        TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_CLASS, PDU_COLUMN_MESSAGE_CLASS);
244        TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_ID, PDU_COLUMN_MESSAGE_ID);
245        TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RESPONSE_TEXT, PDU_COLUMN_RESPONSE_TEXT);
246        TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.TRANSACTION_ID, PDU_COLUMN_TRANSACTION_ID);
247
248        TEXT_STRING_COLUMN_NAME_MAP = new SparseArray<String>();
249        TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_LOCATION, Mms.CONTENT_LOCATION);
250        TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_TYPE, Mms.CONTENT_TYPE);
251        TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_CLASS, Mms.MESSAGE_CLASS);
252        TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_ID, Mms.MESSAGE_ID);
253        TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.RESPONSE_TEXT, Mms.RESPONSE_TEXT);
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();
258        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_CLASS, PDU_COLUMN_CONTENT_CLASS);
259        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_REPORT, PDU_COLUMN_DELIVERY_REPORT);
260        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_TYPE, PDU_COLUMN_MESSAGE_TYPE);
261        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MMS_VERSION, PDU_COLUMN_MMS_VERSION);
262        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.PRIORITY, PDU_COLUMN_PRIORITY);
263        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_REPORT, PDU_COLUMN_READ_REPORT);
264        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_STATUS, PDU_COLUMN_READ_STATUS);
265        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.REPORT_ALLOWED, PDU_COLUMN_REPORT_ALLOWED);
266        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_STATUS, PDU_COLUMN_RETRIEVE_STATUS);
267        OCTET_COLUMN_INDEX_MAP.put(PduHeaders.STATUS, PDU_COLUMN_STATUS);
268
269        OCTET_COLUMN_NAME_MAP = new SparseArray<String>();
270        OCTET_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_CLASS, Mms.CONTENT_CLASS);
271        OCTET_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_REPORT, Mms.DELIVERY_REPORT);
272        OCTET_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_TYPE, Mms.MESSAGE_TYPE);
273        OCTET_COLUMN_NAME_MAP.put(PduHeaders.MMS_VERSION, Mms.MMS_VERSION);
274        OCTET_COLUMN_NAME_MAP.put(PduHeaders.PRIORITY, Mms.PRIORITY);
275        OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_REPORT, Mms.READ_REPORT);
276        OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_STATUS, Mms.READ_STATUS);
277        OCTET_COLUMN_NAME_MAP.put(PduHeaders.REPORT_ALLOWED, Mms.REPORT_ALLOWED);
278        OCTET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_STATUS, Mms.RETRIEVE_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();
283        LONG_COLUMN_INDEX_MAP.put(PduHeaders.DATE, PDU_COLUMN_DATE);
284        LONG_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_TIME, PDU_COLUMN_DELIVERY_TIME);
285        LONG_COLUMN_INDEX_MAP.put(PduHeaders.EXPIRY, PDU_COLUMN_EXPIRY);
286        LONG_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_SIZE, PDU_COLUMN_MESSAGE_SIZE);
287
288        LONG_COLUMN_NAME_MAP = new SparseArray<String>();
289        LONG_COLUMN_NAME_MAP.put(PduHeaders.DATE, Mms.DATE);
290        LONG_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_TIME, Mms.DELIVERY_TIME);
291        LONG_COLUMN_NAME_MAP.put(PduHeaders.EXPIRY, Mms.EXPIRY);
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
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 */
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
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
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
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
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
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
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
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
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     */
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     */
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
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
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
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
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
738    private static String getPartContentType(final PduPart part) {
739        return part.getContentType() == null ? null : toIsoString(part.getContentType());
740    }
741
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
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     */
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 == 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     */
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
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     */
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
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 != 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     */
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) || !partUri.getAuthority().startsWith("mms")) {
1252                    toBeCreated.add(part);
1253                } else {
1254                    toBeUpdated.put(partUri, part);
1255
1256                    // Don't use 'i > 0' to determine whether we should append
1257                    // 'AND' since 'i = 0' may be skipped in another branch.
1258                    if (filter.length() > 1) {
1259                        filter.append(" AND ");
1260                    }
1261
1262                    filter.append(Part._ID);
1263                    filter.append("!=");
1264                    DatabaseUtils.appendEscapedSQLString(filter, partUri.getLastPathSegment());
1265                }
1266            }
1267            filter.append(')');
1268
1269            final long msgId = ContentUris.parseId(uri);
1270
1271            // Remove the parts which doesn't exist anymore.
1272            SqliteWrapper.delete(mContext, mContentResolver,
1273                    Uri.parse(Mms.CONTENT_URI + "/" + msgId + "/part"),
1274                    filter.length() > 2 ? filter.toString() : null, null);
1275
1276            // Create new parts which didn't exist before.
1277            for (final PduPart part : toBeCreated) {
1278                persistPart(part, msgId, preOpenedFiles);
1279            }
1280
1281            // Update the modified parts.
1282            for (final Map.Entry<Uri, PduPart> e : toBeUpdated.entrySet()) {
1283                updatePart(e.getKey(), e.getValue(), preOpenedFiles);
1284            }
1285        } finally {
1286            synchronized (PDU_CACHE_INSTANCE) {
1287                PDU_CACHE_INSTANCE.setUpdating(uri, false);
1288                PDU_CACHE_INSTANCE.notifyAll();
1289            }
1290        }
1291    }
1292
1293    /**
1294     * Persist a PDU object to specific location in the storage.
1295     *
1296     * @param pdu             The PDU object to be stored.
1297     * @param uri             Where to store the given PDU object.
1298     * @param subId           Subscription id associated with this message.
1299     * @param subPhoneNumber TODO
1300     * @param preOpenedFiles  if not null, a map of preopened InputStreams for the parts.
1301     * @return A Uri which can be used to access the stored PDU.
1302     */
1303    public Uri persist(final GenericPdu pdu, final Uri uri, final int subId,
1304            final String subPhoneNumber, final Map<Uri, InputStream> preOpenedFiles)
1305            throws MmsException {
1306        if (uri == null) {
1307            throw new MmsException("Uri may not be null.");
1308        }
1309        long msgId = -1;
1310        try {
1311            msgId = ContentUris.parseId(uri);
1312        } catch (final NumberFormatException e) {
1313            // the uri ends with "inbox" or something else like that
1314        }
1315        final boolean existingUri = msgId != -1;
1316
1317        if (!existingUri && MESSAGE_BOX_MAP.get(uri) == null) {
1318            throw new MmsException(
1319                    "Bad destination, must be one of "
1320                            + "content://mms/inbox, content://mms/sent, "
1321                            + "content://mms/drafts, content://mms/outbox, "
1322                            + "content://mms/temp."
1323            );
1324        }
1325        synchronized (PDU_CACHE_INSTANCE) {
1326            // If the cache item is getting updated, wait until it's done updating before
1327            // purging it.
1328            if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
1329                if (LOCAL_LOGV) {
1330                    LogUtil.v(TAG, "persist: " + uri + " blocked by isUpdating()");
1331                }
1332                try {
1333                    PDU_CACHE_INSTANCE.wait();
1334                } catch (final InterruptedException e) {
1335                    Log.e(TAG, "persist1: ", e);
1336                }
1337            }
1338        }
1339        PDU_CACHE_INSTANCE.purge(uri);
1340
1341        final PduHeaders header = pdu.getPduHeaders();
1342        PduBody body = null;
1343        ContentValues values = new ContentValues();
1344
1345        // Mark new messages as seen in the telephony database so that we don't have to
1346        // do a global "set all messages as seen" since that occasionally seems to be
1347        // problematic (i.e. very slow).  See bug 18189471.
1348        values.put(Mms.SEEN, 1);
1349
1350        //Set<Entry<Integer, String>> set;
1351
1352        for (int i = ENCODED_STRING_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1353            final int field = ENCODED_STRING_COLUMN_NAME_MAP.keyAt(i);
1354            final EncodedStringValue encodedString = header.getEncodedStringValue(field);
1355            if (encodedString != null) {
1356                final String charsetColumn = CHARSET_COLUMN_NAME_MAP.get(field);
1357                values.put(ENCODED_STRING_COLUMN_NAME_MAP.valueAt(i),
1358                        toIsoString(encodedString.getTextString()));
1359                values.put(charsetColumn, encodedString.getCharacterSet());
1360            }
1361        }
1362
1363        for (int i = TEXT_STRING_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1364            final byte[] text = header.getTextString(TEXT_STRING_COLUMN_NAME_MAP.keyAt(i));
1365            if (text != null) {
1366                values.put(TEXT_STRING_COLUMN_NAME_MAP.valueAt(i), toIsoString(text));
1367            }
1368        }
1369
1370        for (int i = OCTET_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1371            final int b = header.getOctet(OCTET_COLUMN_NAME_MAP.keyAt(i));
1372            if (b != 0) {
1373                values.put(OCTET_COLUMN_NAME_MAP.valueAt(i), b);
1374            }
1375        }
1376
1377        for (int i = LONG_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1378            final long l = header.getLongInteger(LONG_COLUMN_NAME_MAP.keyAt(i));
1379            if (l != -1L) {
1380                values.put(LONG_COLUMN_NAME_MAP.valueAt(i), l);
1381            }
1382        }
1383
1384        final SparseArray<EncodedStringValue[]> addressMap =
1385                new SparseArray<EncodedStringValue[]>(ADDRESS_FIELDS.length);
1386        // Save address information.
1387        for (final int addrType : ADDRESS_FIELDS) {
1388            EncodedStringValue[] array = null;
1389            if (addrType == PduHeaders.FROM) {
1390                final EncodedStringValue v = header.getEncodedStringValue(addrType);
1391                if (v != null) {
1392                    array = new EncodedStringValue[1];
1393                    array[0] = v;
1394                }
1395            } else {
1396                array = header.getEncodedStringValues(addrType);
1397            }
1398            addressMap.put(addrType, array);
1399        }
1400
1401        final HashSet<String> recipients = new HashSet<String>();
1402        final int msgType = pdu.getMessageType();
1403        // Here we only allocate thread ID for M-Notification.ind,
1404        // M-Retrieve.conf and M-Send.req.
1405        // Some of other PDU types may be allocated a thread ID outside
1406        // this scope.
1407        if ((msgType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND)
1408                || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)
1409                || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) {
1410            switch (msgType) {
1411                case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
1412                case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF:
1413                    loadRecipients(PduHeaders.FROM, recipients, addressMap);
1414
1415                    // For received messages (whether group MMS is enabled or not) we want to
1416                    // associate this message with the thread composed of all the recipients
1417                    // EXCLUDING our own number. This includes the person who sent the
1418                    // message (the FROM field above) in addition to the other people the message
1419                    // was addressed TO (or CC fields to address group messaging compatibility
1420                    // issues with devices that place numbers in this field). Typically our own
1421                    // number is in the TO/CC field so we have to remove it in loadRecipients.
1422                    checkAndLoadToCcRecipients(recipients, addressMap, subPhoneNumber);
1423                    break;
1424                case PduHeaders.MESSAGE_TYPE_SEND_REQ:
1425                    loadRecipients(PduHeaders.TO, recipients, addressMap);
1426                    break;
1427            }
1428            long threadId = -1L;
1429            if (!recipients.isEmpty()) {
1430                // Given all the recipients associated with this message, find (or create) the
1431                // correct thread.
1432                threadId = MmsSmsUtils.Threads.getOrCreateThreadId(mContext, recipients);
1433            } else {
1434                LogUtil.w(TAG, "PduPersister.persist No recipients; persisting PDU to thread: "
1435                        + threadId);
1436            }
1437            values.put(Mms.THREAD_ID, threadId);
1438        }
1439
1440        // Save parts first to avoid inconsistent message is loaded
1441        // while saving the parts.
1442        final long dummyId = System.currentTimeMillis(); // Dummy ID of the msg.
1443
1444        // Figure out if this PDU is a text-only message
1445        boolean textOnly = true;
1446
1447        // Get body if the PDU is a RetrieveConf or SendReq.
1448        if (pdu instanceof MultimediaMessagePdu) {
1449            body = ((MultimediaMessagePdu) pdu).getBody();
1450            // Start saving parts if necessary.
1451            if (body != null) {
1452                final int partsNum = body.getPartsNum();
1453                if (LOCAL_LOGV) {
1454                    LogUtil.v(TAG, "PduPersister.persist partsNum: " + partsNum);
1455                }
1456                if (partsNum > 2) {
1457                    // For a text-only message there will be two parts: 1-the SMIL, 2-the text.
1458                    // Down a few lines below we're checking to make sure we've only got SMIL or
1459                    // text. We also have to check then we don't have more than two parts.
1460                    // Otherwise, a slideshow with two text slides would be marked as textOnly.
1461                    textOnly = false;
1462                }
1463                for (int i = 0; i < partsNum; i++) {
1464                    final PduPart part = body.getPart(i);
1465                    persistPart(part, dummyId, preOpenedFiles);
1466
1467                    // If we've got anything besides text/plain or SMIL part, then we've got
1468                    // an mms message with some other type of attachment.
1469                    final String contentType = getPartContentType(part);
1470                    if (LOCAL_LOGV) {
1471                        LogUtil.v(TAG, "PduPersister.persist part: " + i + " contentType: " +
1472                                contentType);
1473                    }
1474                    if (contentType != null && !ContentType.APP_SMIL.equals(contentType)
1475                            && !ContentType.TEXT_PLAIN.equals(contentType)) {
1476                        textOnly = false;
1477                    }
1478                }
1479            }
1480        }
1481        // Record whether this mms message is a simple plain text or not. This is a hint for the
1482        // UI.
1483        if (OsUtil.isAtLeastJB_MR1()) {
1484            values.put(Mms.TEXT_ONLY, textOnly ? 1 : 0);
1485        }
1486
1487        if (OsUtil.isAtLeastL_MR1()) {
1488            values.put(Mms.SUBSCRIPTION_ID, subId);
1489        } else {
1490            Assert.equals(ParticipantData.DEFAULT_SELF_SUB_ID, subId);
1491        }
1492
1493        Uri res = null;
1494        if (existingUri) {
1495            res = uri;
1496            SqliteWrapper.update(mContext, mContentResolver, res, values, null, null);
1497        } else {
1498            res = SqliteWrapper.insert(mContext, mContentResolver, uri, values);
1499            if (res == null) {
1500                throw new MmsException("persist() failed: return null.");
1501            }
1502            // Get the real ID of the PDU and update all parts which were
1503            // saved with the dummy ID.
1504            msgId = ContentUris.parseId(res);
1505        }
1506
1507        values = new ContentValues(1);
1508        values.put(Part.MSG_ID, msgId);
1509        SqliteWrapper.update(mContext, mContentResolver,
1510                Uri.parse("content://mms/" + dummyId + "/part"),
1511                values, null, null);
1512        // We should return the longest URI of the persisted PDU, for
1513        // example, if input URI is "content://mms/inbox" and the _ID of
1514        // persisted PDU is '8', we should return "content://mms/inbox/8"
1515        // instead of "content://mms/8".
1516        // TODO: Should the MmsProvider be responsible for this???
1517        if (!existingUri) {
1518            res = Uri.parse(uri + "/" + msgId);
1519        }
1520
1521        // Save address information.
1522        for (final int addrType : ADDRESS_FIELDS) {
1523            final EncodedStringValue[] array = addressMap.get(addrType);
1524            if (array != null) {
1525                persistAddress(msgId, addrType, array);
1526            }
1527        }
1528
1529        return res;
1530    }
1531
1532    /**
1533     * For a given address type, extract the recipients from the headers.
1534     *
1535     * @param addressType     can be PduHeaders.FROM or PduHeaders.TO
1536     * @param recipients      a HashSet that is loaded with the recipients from the FROM or TO
1537     *                        headers
1538     * @param addressMap      a HashMap of the addresses from the ADDRESS_FIELDS header
1539     */
1540    private void loadRecipients(final int addressType, final HashSet<String> recipients,
1541            final SparseArray<EncodedStringValue[]> addressMap) {
1542        final EncodedStringValue[] array = addressMap.get(addressType);
1543        if (array == null) {
1544            return;
1545        }
1546        for (final EncodedStringValue v : array) {
1547            if (v != null) {
1548                final String number = v.getString();
1549                if (!recipients.contains(number)) {
1550                    // Only add numbers which aren't already included.
1551                    recipients.add(number);
1552                }
1553            }
1554        }
1555    }
1556
1557    /**
1558     * For a given address type, extract the recipients from the headers.
1559     *
1560     * @param recipients      a HashSet that is loaded with the recipients from the FROM or TO
1561     *                        headers
1562     * @param addressMap      a HashMap of the addresses from the ADDRESS_FIELDS header
1563     * @param selfNumber      self phone number
1564     */
1565    private void checkAndLoadToCcRecipients(final HashSet<String> recipients,
1566            final SparseArray<EncodedStringValue[]> addressMap, final String selfNumber) {
1567        final EncodedStringValue[] arrayTo = addressMap.get(PduHeaders.TO);
1568        final EncodedStringValue[] arrayCc = addressMap.get(PduHeaders.CC);
1569        final ArrayList<String> numbers = new ArrayList<String>();
1570        if (arrayTo != null) {
1571            for (final EncodedStringValue v : arrayTo) {
1572                if (v != null) {
1573                    numbers.add(v.getString());
1574                }
1575            }
1576        }
1577        if (arrayCc != null) {
1578            for (final EncodedStringValue v : arrayCc) {
1579                if (v != null) {
1580                    numbers.add(v.getString());
1581                }
1582            }
1583        }
1584        for (final String number : numbers) {
1585            // Only add numbers which aren't my own number.
1586            if (TextUtils.isEmpty(selfNumber) || !PhoneNumberUtils.compare(number, selfNumber)) {
1587                if (!recipients.contains(number)) {
1588                    // Only add numbers which aren't already included.
1589                    recipients.add(number);
1590                }
1591            }
1592        }
1593    }
1594
1595    /**
1596     * Move a PDU object from one location to another.
1597     *
1598     * @param from Specify the PDU object to be moved.
1599     * @param to   The destination location, should be one of the following:
1600     *             "content://mms/inbox", "content://mms/sent",
1601     *             "content://mms/drafts", "content://mms/outbox",
1602     *             "content://mms/trash".
1603     * @return New Uri of the moved PDU.
1604     * @throws MmsException Error occurred while moving the message.
1605     */
1606    public Uri move(final Uri from, final Uri to) throws MmsException {
1607        // Check whether the 'msgId' has been assigned a valid value.
1608        final long msgId = ContentUris.parseId(from);
1609        if (msgId == -1L) {
1610            throw new MmsException("Error! ID of the message: -1.");
1611        }
1612
1613        // Get corresponding int value of destination box.
1614        final Integer msgBox = MESSAGE_BOX_MAP.get(to);
1615        if (msgBox == null) {
1616            throw new MmsException(
1617                    "Bad destination, must be one of "
1618                            + "content://mms/inbox, content://mms/sent, "
1619                            + "content://mms/drafts, content://mms/outbox, "
1620                            + "content://mms/temp."
1621            );
1622        }
1623
1624        final ContentValues values = new ContentValues(1);
1625        values.put(Mms.MESSAGE_BOX, msgBox);
1626        SqliteWrapper.update(mContext, mContentResolver, from, values, null, null);
1627        return ContentUris.withAppendedId(to, msgId);
1628    }
1629
1630    /**
1631     * Wrap a byte[] into a String.
1632     */
1633    public static String toIsoString(final byte[] bytes) {
1634        try {
1635            return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
1636        } catch (final UnsupportedEncodingException e) {
1637            // Impossible to reach here!
1638            Log.e(TAG, "ISO_8859_1 must be supported!", e);
1639            return "";
1640        }
1641    }
1642
1643    /**
1644     * Unpack a given String into a byte[].
1645     */
1646    public static byte[] getBytes(final String data) {
1647        try {
1648            return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
1649        } catch (final UnsupportedEncodingException e) {
1650            // Impossible to reach here!
1651            Log.e(TAG, "ISO_8859_1 must be supported!", e);
1652            return new byte[0];
1653        }
1654    }
1655
1656    /**
1657     * Remove all objects in the temporary path.
1658     */
1659    public void release() {
1660        final Uri uri = Uri.parse(TEMPORARY_DRM_OBJECT_URI);
1661        SqliteWrapper.delete(mContext, mContentResolver, uri, null, null);
1662    }
1663
1664    /**
1665     * Find all messages to be sent or downloaded before certain time.
1666     */
1667    public Cursor getPendingMessages(final long dueTime) {
1668        final Uri.Builder uriBuilder = PendingMessages.CONTENT_URI.buildUpon();
1669        uriBuilder.appendQueryParameter("protocol", "mms");
1670
1671        final String selection = PendingMessages.ERROR_TYPE + " < ?"
1672                + " AND " + PendingMessages.DUE_TIME + " <= ?";
1673
1674        final String[] selectionArgs = new String[] {
1675                String.valueOf(MmsSms.ERR_TYPE_GENERIC_PERMANENT),
1676                String.valueOf(dueTime)
1677        };
1678
1679        return SqliteWrapper.query(mContext, mContentResolver,
1680                uriBuilder.build(), null, selection, selectionArgs,
1681                PendingMessages.DUE_TIME);
1682    }
1683}
1684