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