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, HashMap<Uri, InputStream> preOpenedFiles)
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, preOpenedFiles);
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     * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts.
777     * @throws MmsException Cannot find source data or error occurred
778     *         while saving the data.
779     */
780    private void persistData(PduPart part, Uri uri,
781            String contentType, HashMap<Uri, InputStream> preOpenedFiles)
782            throws MmsException {
783        OutputStream os = null;
784        InputStream is = null;
785        DrmConvertSession drmConvertSession = null;
786        Uri dataUri = null;
787        String path = null;
788
789        try {
790            byte[] data = part.getData();
791            if (ContentType.TEXT_PLAIN.equals(contentType)
792                    || ContentType.APP_SMIL.equals(contentType)
793                    || ContentType.TEXT_HTML.equals(contentType)) {
794                ContentValues cv = new ContentValues();
795                cv.put(Telephony.Mms.Part.TEXT, new EncodedStringValue(data).getString());
796                if (mContentResolver.update(uri, cv, null, null) != 1) {
797                    throw new MmsException("unable to update " + uri.toString());
798                }
799            } else {
800                boolean isDrm = DownloadDrmHelper.isDrmConvertNeeded(contentType);
801                if (isDrm) {
802                    if (uri != null) {
803                        try {
804                            path = convertUriToPath(mContext, uri);
805                            if (LOCAL_LOGV) {
806                                Log.v(TAG, "drm uri: " + uri + " path: " + path);
807                            }
808                            File f = new File(path);
809                            long len = f.length();
810                            if (LOCAL_LOGV) {
811                                Log.v(TAG, "drm path: " + path + " len: " + len);
812                            }
813                            if (len > 0) {
814                                // we're not going to re-persist and re-encrypt an already
815                                // converted drm file
816                                return;
817                            }
818                        } catch (Exception e) {
819                            Log.e(TAG, "Can't get file info for: " + part.getDataUri(), e);
820                        }
821                    }
822                    // We haven't converted the file yet, start the conversion
823                    drmConvertSession = DrmConvertSession.open(mContext, contentType);
824                    if (drmConvertSession == null) {
825                        throw new MmsException("Mimetype " + contentType +
826                                " can not be converted.");
827                    }
828                }
829                // uri can look like:
830                // content://mms/part/98
831                os = mContentResolver.openOutputStream(uri);
832                if (data == null) {
833                    dataUri = part.getDataUri();
834                    if ((dataUri == null) || (dataUri == uri)) {
835                        Log.w(TAG, "Can't find data for this part.");
836                        return;
837                    }
838                    // dataUri can look like:
839                    // content://com.google.android.gallery3d.provider/picasa/item/5720646660183715586
840                    if (preOpenedFiles != null && preOpenedFiles.containsKey(dataUri)) {
841                        is = preOpenedFiles.get(dataUri);
842                    }
843                    if (is == null) {
844                        is = mContentResolver.openInputStream(dataUri);
845                    }
846
847                    if (LOCAL_LOGV) {
848                        Log.v(TAG, "Saving data to: " + uri);
849                    }
850
851                    byte[] buffer = new byte[8192];
852                    for (int len = 0; (len = is.read(buffer)) != -1; ) {
853                        if (!isDrm) {
854                            os.write(buffer, 0, len);
855                        } else {
856                            byte[] convertedData = drmConvertSession.convert(buffer, len);
857                            if (convertedData != null) {
858                                os.write(convertedData, 0, convertedData.length);
859                            } else {
860                                throw new MmsException("Error converting drm data.");
861                            }
862                        }
863                    }
864                } else {
865                    if (LOCAL_LOGV) {
866                        Log.v(TAG, "Saving data to: " + uri);
867                    }
868                    if (!isDrm) {
869                        os.write(data);
870                    } else {
871                        dataUri = uri;
872                        byte[] convertedData = drmConvertSession.convert(data, data.length);
873                        if (convertedData != null) {
874                            os.write(convertedData, 0, convertedData.length);
875                        } else {
876                            throw new MmsException("Error converting drm data.");
877                        }
878                    }
879                }
880            }
881        } catch (FileNotFoundException e) {
882            Log.e(TAG, "Failed to open Input/Output stream.", e);
883            throw new MmsException(e);
884        } catch (IOException e) {
885            Log.e(TAG, "Failed to read/write data.", e);
886            throw new MmsException(e);
887        } finally {
888            if (os != null) {
889                try {
890                    os.close();
891                } catch (IOException e) {
892                    Log.e(TAG, "IOException while closing: " + os, e);
893                } // Ignore
894            }
895            if (is != null) {
896                try {
897                    is.close();
898                } catch (IOException e) {
899                    Log.e(TAG, "IOException while closing: " + is, e);
900                } // Ignore
901            }
902            if (drmConvertSession != null) {
903                drmConvertSession.close(path);
904
905                // Reset the permissions on the encrypted part file so everyone has only read
906                // permission.
907                File f = new File(path);
908                ContentValues values = new ContentValues(0);
909                SqliteWrapper.update(mContext, mContentResolver,
910                                     Uri.parse("content://mms/resetFilePerm/" + f.getName()),
911                                     values, null, null);
912            }
913        }
914    }
915
916    /**
917     * This method expects uri in the following format
918     *     content://media/<table_name>/<row_index> (or)
919     *     file://sdcard/test.mp4
920     *     http://test.com/test.mp4
921     *
922     * Here <table_name> shall be "video" or "audio" or "images"
923     * <row_index> the index of the content in given table
924     */
925    static public String convertUriToPath(Context context, Uri uri) {
926        String path = null;
927        if (null != uri) {
928            String scheme = uri.getScheme();
929            if (null == scheme || scheme.equals("") ||
930                    scheme.equals(ContentResolver.SCHEME_FILE)) {
931                path = uri.getPath();
932
933            } else if (scheme.equals("http")) {
934                path = uri.toString();
935
936            } else if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
937                String[] projection = new String[] {MediaStore.MediaColumns.DATA};
938                Cursor cursor = null;
939                try {
940                    cursor = context.getContentResolver().query(uri, projection, null,
941                            null, null);
942                    if (null == cursor || 0 == cursor.getCount() || !cursor.moveToFirst()) {
943                        throw new IllegalArgumentException("Given Uri could not be found" +
944                                " in media store");
945                    }
946                    int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
947                    path = cursor.getString(pathIndex);
948                } catch (SQLiteException e) {
949                    throw new IllegalArgumentException("Given Uri is not formatted in a way " +
950                            "so that it can be found in media store.");
951                } finally {
952                    if (null != cursor) {
953                        cursor.close();
954                    }
955                }
956            } else {
957                throw new IllegalArgumentException("Given Uri scheme is not supported");
958            }
959        }
960        return path;
961    }
962
963    private void updateAddress(
964            long msgId, int type, EncodedStringValue[] array) {
965        // Delete old address information and then insert new ones.
966        SqliteWrapper.delete(mContext, mContentResolver,
967                Uri.parse("content://mms/" + msgId + "/addr"),
968                Addr.TYPE + "=" + type, null);
969
970        persistAddress(msgId, type, array);
971    }
972
973    /**
974     * Update headers of a SendReq.
975     *
976     * @param uri The PDU which need to be updated.
977     * @param pdu New headers.
978     * @throws MmsException Bad URI or updating failed.
979     */
980    public void updateHeaders(Uri uri, SendReq sendReq) {
981        synchronized(PDU_CACHE_INSTANCE) {
982            // If the cache item is getting updated, wait until it's done updating before
983            // purging it.
984            if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
985                if (LOCAL_LOGV) {
986                    Log.v(TAG, "updateHeaders: " + uri + " blocked by isUpdating()");
987                }
988                try {
989                    PDU_CACHE_INSTANCE.wait();
990                } catch (InterruptedException e) {
991                    Log.e(TAG, "updateHeaders: ", e);
992                }
993            }
994        }
995        PDU_CACHE_INSTANCE.purge(uri);
996
997        ContentValues values = new ContentValues(10);
998        byte[] contentType = sendReq.getContentType();
999        if (contentType != null) {
1000            values.put(Mms.CONTENT_TYPE, toIsoString(contentType));
1001        }
1002
1003        long date = sendReq.getDate();
1004        if (date != -1) {
1005            values.put(Mms.DATE, date);
1006        }
1007
1008        int deliveryReport = sendReq.getDeliveryReport();
1009        if (deliveryReport != 0) {
1010            values.put(Mms.DELIVERY_REPORT, deliveryReport);
1011        }
1012
1013        long expiry = sendReq.getExpiry();
1014        if (expiry != -1) {
1015            values.put(Mms.EXPIRY, expiry);
1016        }
1017
1018        byte[] msgClass = sendReq.getMessageClass();
1019        if (msgClass != null) {
1020            values.put(Mms.MESSAGE_CLASS, toIsoString(msgClass));
1021        }
1022
1023        int priority = sendReq.getPriority();
1024        if (priority != 0) {
1025            values.put(Mms.PRIORITY, priority);
1026        }
1027
1028        int readReport = sendReq.getReadReport();
1029        if (readReport != 0) {
1030            values.put(Mms.READ_REPORT, readReport);
1031        }
1032
1033        byte[] transId = sendReq.getTransactionId();
1034        if (transId != null) {
1035            values.put(Mms.TRANSACTION_ID, toIsoString(transId));
1036        }
1037
1038        EncodedStringValue subject = sendReq.getSubject();
1039        if (subject != null) {
1040            values.put(Mms.SUBJECT, toIsoString(subject.getTextString()));
1041            values.put(Mms.SUBJECT_CHARSET, subject.getCharacterSet());
1042        } else {
1043            values.put(Mms.SUBJECT, "");
1044        }
1045
1046        long messageSize = sendReq.getMessageSize();
1047        if (messageSize > 0) {
1048            values.put(Mms.MESSAGE_SIZE, messageSize);
1049        }
1050
1051        PduHeaders headers = sendReq.getPduHeaders();
1052        HashSet<String> recipients = new HashSet<String>();
1053        for (int addrType : ADDRESS_FIELDS) {
1054            EncodedStringValue[] array = null;
1055            if (addrType == PduHeaders.FROM) {
1056                EncodedStringValue v = headers.getEncodedStringValue(addrType);
1057                if (v != null) {
1058                    array = new EncodedStringValue[1];
1059                    array[0] = v;
1060                }
1061            } else {
1062                array = headers.getEncodedStringValues(addrType);
1063            }
1064
1065            if (array != null) {
1066                long msgId = ContentUris.parseId(uri);
1067                updateAddress(msgId, addrType, array);
1068                if (addrType == PduHeaders.TO) {
1069                    for (EncodedStringValue v : array) {
1070                        if (v != null) {
1071                            recipients.add(v.getString());
1072                        }
1073                    }
1074                }
1075            }
1076        }
1077        if (!recipients.isEmpty()) {
1078            long threadId = Threads.getOrCreateThreadId(mContext, recipients);
1079            values.put(Mms.THREAD_ID, threadId);
1080        }
1081
1082        SqliteWrapper.update(mContext, mContentResolver, uri, values, null, null);
1083    }
1084
1085    private void updatePart(Uri uri, PduPart part, HashMap<Uri, InputStream> preOpenedFiles)
1086            throws MmsException {
1087        ContentValues values = new ContentValues(7);
1088
1089        int charset = part.getCharset();
1090        if (charset != 0 ) {
1091            values.put(Part.CHARSET, charset);
1092        }
1093
1094        String contentType = null;
1095        if (part.getContentType() != null) {
1096            contentType = toIsoString(part.getContentType());
1097            values.put(Part.CONTENT_TYPE, contentType);
1098        } else {
1099            throw new MmsException("MIME type of the part must be set.");
1100        }
1101
1102        if (part.getFilename() != null) {
1103            String fileName = new String(part.getFilename());
1104            values.put(Part.FILENAME, fileName);
1105        }
1106
1107        if (part.getName() != null) {
1108            String name = new String(part.getName());
1109            values.put(Part.NAME, name);
1110        }
1111
1112        Object value = null;
1113        if (part.getContentDisposition() != null) {
1114            value = toIsoString(part.getContentDisposition());
1115            values.put(Part.CONTENT_DISPOSITION, (String) value);
1116        }
1117
1118        if (part.getContentId() != null) {
1119            value = toIsoString(part.getContentId());
1120            values.put(Part.CONTENT_ID, (String) value);
1121        }
1122
1123        if (part.getContentLocation() != null) {
1124            value = toIsoString(part.getContentLocation());
1125            values.put(Part.CONTENT_LOCATION, (String) value);
1126        }
1127
1128        SqliteWrapper.update(mContext, mContentResolver, uri, values, null, null);
1129
1130        // Only update the data when:
1131        // 1. New binary data supplied or
1132        // 2. The Uri of the part is different from the current one.
1133        if ((part.getData() != null)
1134                || (uri != part.getDataUri())) {
1135            persistData(part, uri, contentType, preOpenedFiles);
1136        }
1137    }
1138
1139    /**
1140     * Update all parts of a PDU.
1141     *
1142     * @param uri The PDU which need to be updated.
1143     * @param body New message body of the PDU.
1144     * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts.
1145     * @throws MmsException Bad URI or updating failed.
1146     */
1147    public void updateParts(Uri uri, PduBody body, HashMap<Uri, InputStream> preOpenedFiles)
1148            throws MmsException {
1149        try {
1150            PduCacheEntry cacheEntry;
1151            synchronized(PDU_CACHE_INSTANCE) {
1152                if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
1153                    if (LOCAL_LOGV) {
1154                        Log.v(TAG, "updateParts: " + uri + " blocked by isUpdating()");
1155                    }
1156                    try {
1157                        PDU_CACHE_INSTANCE.wait();
1158                    } catch (InterruptedException e) {
1159                        Log.e(TAG, "updateParts: ", e);
1160                    }
1161                    cacheEntry = PDU_CACHE_INSTANCE.get(uri);
1162                    if (cacheEntry != null) {
1163                        ((MultimediaMessagePdu) cacheEntry.getPdu()).setBody(body);
1164                    }
1165                }
1166                // Tell the cache to indicate to other callers that this item
1167                // is currently being updated.
1168                PDU_CACHE_INSTANCE.setUpdating(uri, true);
1169            }
1170
1171            ArrayList<PduPart> toBeCreated = new ArrayList<PduPart>();
1172            HashMap<Uri, PduPart> toBeUpdated = new HashMap<Uri, PduPart>();
1173
1174            int partsNum = body.getPartsNum();
1175            StringBuilder filter = new StringBuilder().append('(');
1176            for (int i = 0; i < partsNum; i++) {
1177                PduPart part = body.getPart(i);
1178                Uri partUri = part.getDataUri();
1179                if ((partUri == null) || !partUri.getAuthority().startsWith("mms")) {
1180                    toBeCreated.add(part);
1181                } else {
1182                    toBeUpdated.put(partUri, part);
1183
1184                    // Don't use 'i > 0' to determine whether we should append
1185                    // 'AND' since 'i = 0' may be skipped in another branch.
1186                    if (filter.length() > 1) {
1187                        filter.append(" AND ");
1188                    }
1189
1190                    filter.append(Part._ID);
1191                    filter.append("!=");
1192                    DatabaseUtils.appendEscapedSQLString(filter, partUri.getLastPathSegment());
1193                }
1194            }
1195            filter.append(')');
1196
1197            long msgId = ContentUris.parseId(uri);
1198
1199            // Remove the parts which doesn't exist anymore.
1200            SqliteWrapper.delete(mContext, mContentResolver,
1201                    Uri.parse(Mms.CONTENT_URI + "/" + msgId + "/part"),
1202                    filter.length() > 2 ? filter.toString() : null, null);
1203
1204            // Create new parts which didn't exist before.
1205            for (PduPart part : toBeCreated) {
1206                persistPart(part, msgId, preOpenedFiles);
1207            }
1208
1209            // Update the modified parts.
1210            for (Map.Entry<Uri, PduPart> e : toBeUpdated.entrySet()) {
1211                updatePart(e.getKey(), e.getValue(), preOpenedFiles);
1212            }
1213        } finally {
1214            synchronized(PDU_CACHE_INSTANCE) {
1215                PDU_CACHE_INSTANCE.setUpdating(uri, false);
1216                PDU_CACHE_INSTANCE.notifyAll();
1217            }
1218        }
1219    }
1220
1221    /**
1222     * Persist a PDU object to specific location in the storage.
1223     *
1224     * @param pdu The PDU object to be stored.
1225     * @param uri Where to store the given PDU object.
1226     * @param createThreadId if true, this function may create a thread id for the recipients
1227     * @param groupMmsEnabled if true, all of the recipients addressed in the PDU will be used
1228     *  to create the associated thread. When false, only the sender will be used in finding or
1229     *  creating the appropriate thread or conversation.
1230     * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts.
1231     * @return A Uri which can be used to access the stored PDU.
1232     */
1233
1234    public Uri persist(GenericPdu pdu, Uri uri, boolean createThreadId, boolean groupMmsEnabled,
1235            HashMap<Uri, InputStream> preOpenedFiles)
1236            throws MmsException {
1237        if (uri == null) {
1238            throw new MmsException("Uri may not be null.");
1239        }
1240        long msgId = -1;
1241        try {
1242            msgId = ContentUris.parseId(uri);
1243        } catch (NumberFormatException e) {
1244            // the uri ends with "inbox" or something else like that
1245        }
1246        boolean existingUri = msgId != -1;
1247
1248        if (!existingUri && MESSAGE_BOX_MAP.get(uri) == null) {
1249            throw new MmsException(
1250                    "Bad destination, must be one of "
1251                    + "content://mms/inbox, content://mms/sent, "
1252                    + "content://mms/drafts, content://mms/outbox, "
1253                    + "content://mms/temp.");
1254        }
1255        synchronized(PDU_CACHE_INSTANCE) {
1256            // If the cache item is getting updated, wait until it's done updating before
1257            // purging it.
1258            if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
1259                if (LOCAL_LOGV) {
1260                    Log.v(TAG, "persist: " + uri + " blocked by isUpdating()");
1261                }
1262                try {
1263                    PDU_CACHE_INSTANCE.wait();
1264                } catch (InterruptedException e) {
1265                    Log.e(TAG, "persist1: ", e);
1266                }
1267            }
1268        }
1269        PDU_CACHE_INSTANCE.purge(uri);
1270
1271        PduHeaders header = pdu.getPduHeaders();
1272        PduBody body = null;
1273        ContentValues values = new ContentValues();
1274        Set<Entry<Integer, String>> set;
1275
1276        set = ENCODED_STRING_COLUMN_NAME_MAP.entrySet();
1277        for (Entry<Integer, String> e : set) {
1278            int field = e.getKey();
1279            EncodedStringValue encodedString = header.getEncodedStringValue(field);
1280            if (encodedString != null) {
1281                String charsetColumn = CHARSET_COLUMN_NAME_MAP.get(field);
1282                values.put(e.getValue(), toIsoString(encodedString.getTextString()));
1283                values.put(charsetColumn, encodedString.getCharacterSet());
1284            }
1285        }
1286
1287        set = TEXT_STRING_COLUMN_NAME_MAP.entrySet();
1288        for (Entry<Integer, String> e : set){
1289            byte[] text = header.getTextString(e.getKey());
1290            if (text != null) {
1291                values.put(e.getValue(), toIsoString(text));
1292            }
1293        }
1294
1295        set = OCTET_COLUMN_NAME_MAP.entrySet();
1296        for (Entry<Integer, String> e : set){
1297            int b = header.getOctet(e.getKey());
1298            if (b != 0) {
1299                values.put(e.getValue(), b);
1300            }
1301        }
1302
1303        set = LONG_COLUMN_NAME_MAP.entrySet();
1304        for (Entry<Integer, String> e : set){
1305            long l = header.getLongInteger(e.getKey());
1306            if (l != -1L) {
1307                values.put(e.getValue(), l);
1308            }
1309        }
1310
1311        HashMap<Integer, EncodedStringValue[]> addressMap =
1312                new HashMap<Integer, EncodedStringValue[]>(ADDRESS_FIELDS.length);
1313        // Save address information.
1314        for (int addrType : ADDRESS_FIELDS) {
1315            EncodedStringValue[] array = null;
1316            if (addrType == PduHeaders.FROM) {
1317                EncodedStringValue v = header.getEncodedStringValue(addrType);
1318                if (v != null) {
1319                    array = new EncodedStringValue[1];
1320                    array[0] = v;
1321                }
1322            } else {
1323                array = header.getEncodedStringValues(addrType);
1324            }
1325            addressMap.put(addrType, array);
1326        }
1327
1328        HashSet<String> recipients = new HashSet<String>();
1329        int msgType = pdu.getMessageType();
1330        // Here we only allocate thread ID for M-Notification.ind,
1331        // M-Retrieve.conf and M-Send.req.
1332        // Some of other PDU types may be allocated a thread ID outside
1333        // this scope.
1334        if ((msgType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND)
1335                || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)
1336                || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) {
1337            switch (msgType) {
1338                case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
1339                case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF:
1340                    loadRecipients(PduHeaders.FROM, recipients, addressMap, false);
1341
1342                    // For received messages when group MMS is enabled, we want to associate this
1343                    // message with the thread composed of all the recipients -- all but our own
1344                    // number, that is. This includes the person who sent the
1345                    // message or the FROM field (above) in addition to the other people the message
1346                    // was addressed to or the TO field. Our own number is in that TO field and
1347                    // we have to ignore it in loadRecipients.
1348                    if (groupMmsEnabled) {
1349                        loadRecipients(PduHeaders.TO, recipients, addressMap, true);
1350                    }
1351                    break;
1352                case PduHeaders.MESSAGE_TYPE_SEND_REQ:
1353                    loadRecipients(PduHeaders.TO, recipients, addressMap, false);
1354                    break;
1355            }
1356            long threadId = 0;
1357            if (createThreadId && !recipients.isEmpty()) {
1358                // Given all the recipients associated with this message, find (or create) the
1359                // correct thread.
1360                threadId = Threads.getOrCreateThreadId(mContext, recipients);
1361            }
1362            values.put(Mms.THREAD_ID, threadId);
1363        }
1364
1365        // Save parts first to avoid inconsistent message is loaded
1366        // while saving the parts.
1367        long dummyId = System.currentTimeMillis(); // Dummy ID of the msg.
1368
1369        // Figure out if this PDU is a text-only message
1370        boolean textOnly = true;
1371
1372        // Get body if the PDU is a RetrieveConf or SendReq.
1373        if (pdu instanceof MultimediaMessagePdu) {
1374            body = ((MultimediaMessagePdu) pdu).getBody();
1375            // Start saving parts if necessary.
1376            if (body != null) {
1377                int partsNum = body.getPartsNum();
1378                if (partsNum > 2) {
1379                    // For a text-only message there will be two parts: 1-the SMIL, 2-the text.
1380                    // Down a few lines below we're checking to make sure we've only got SMIL or
1381                    // text. We also have to check then we don't have more than two parts.
1382                    // Otherwise, a slideshow with two text slides would be marked as textOnly.
1383                    textOnly = false;
1384                }
1385                for (int i = 0; i < partsNum; i++) {
1386                    PduPart part = body.getPart(i);
1387                    persistPart(part, dummyId, preOpenedFiles);
1388
1389                    // If we've got anything besides text/plain or SMIL part, then we've got
1390                    // an mms message with some other type of attachment.
1391                    String contentType = getPartContentType(part);
1392                    if (contentType != null && !ContentType.APP_SMIL.equals(contentType)
1393                            && !ContentType.TEXT_PLAIN.equals(contentType)) {
1394                        textOnly = false;
1395                    }
1396                }
1397            }
1398        }
1399        // Record whether this mms message is a simple plain text or not. This is a hint for the
1400        // UI.
1401        values.put(Mms.TEXT_ONLY, textOnly ? 1 : 0);
1402
1403        Uri res = null;
1404        if (existingUri) {
1405            res = uri;
1406            SqliteWrapper.update(mContext, mContentResolver, res, values, null, null);
1407        } else {
1408            res = SqliteWrapper.insert(mContext, mContentResolver, uri, values);
1409            if (res == null) {
1410                throw new MmsException("persist() failed: return null.");
1411            }
1412            // Get the real ID of the PDU and update all parts which were
1413            // saved with the dummy ID.
1414            msgId = ContentUris.parseId(res);
1415        }
1416
1417        values = new ContentValues(1);
1418        values.put(Part.MSG_ID, msgId);
1419        SqliteWrapper.update(mContext, mContentResolver,
1420                             Uri.parse("content://mms/" + dummyId + "/part"),
1421                             values, null, null);
1422        // We should return the longest URI of the persisted PDU, for
1423        // example, if input URI is "content://mms/inbox" and the _ID of
1424        // persisted PDU is '8', we should return "content://mms/inbox/8"
1425        // instead of "content://mms/8".
1426        // FIXME: Should the MmsProvider be responsible for this???
1427        if (!existingUri) {
1428            res = Uri.parse(uri + "/" + msgId);
1429        }
1430
1431        // Save address information.
1432        for (int addrType : ADDRESS_FIELDS) {
1433            EncodedStringValue[] array = addressMap.get(addrType);
1434            if (array != null) {
1435                persistAddress(msgId, addrType, array);
1436            }
1437        }
1438
1439        return res;
1440    }
1441
1442    /**
1443     * For a given address type, extract the recipients from the headers.
1444     *
1445     * @param addressType can be PduHeaders.FROM or PduHeaders.TO
1446     * @param recipients a HashSet that is loaded with the recipients from the FROM or TO headers
1447     * @param addressMap a HashMap of the addresses from the ADDRESS_FIELDS header
1448     * @param excludeMyNumber if true, the number of this phone will be excluded from recipients
1449     */
1450    private void loadRecipients(int addressType, HashSet<String> recipients,
1451            HashMap<Integer, EncodedStringValue[]> addressMap, boolean excludeMyNumber) {
1452        EncodedStringValue[] array = addressMap.get(addressType);
1453        if (array == null) {
1454            return;
1455        }
1456        // If the TO recipients is only a single address, then we can skip loadRecipients when
1457        // we're excluding our own number because we know that address is our own.
1458        if (excludeMyNumber && array.length == 1) {
1459            return;
1460        }
1461        String myNumber = excludeMyNumber ? mTelephonyManager.getLine1Number() : null;
1462        for (EncodedStringValue v : array) {
1463            if (v != null) {
1464                String number = v.getString();
1465                if ((myNumber == null || !PhoneNumberUtils.compare(number, myNumber)) &&
1466                        !recipients.contains(number)) {
1467                    // Only add numbers which aren't my own number.
1468                    recipients.add(number);
1469                }
1470            }
1471        }
1472    }
1473
1474    /**
1475     * Move a PDU object from one location to another.
1476     *
1477     * @param from Specify the PDU object to be moved.
1478     * @param to The destination location, should be one of the following:
1479     *        "content://mms/inbox", "content://mms/sent",
1480     *        "content://mms/drafts", "content://mms/outbox",
1481     *        "content://mms/trash".
1482     * @return New Uri of the moved PDU.
1483     * @throws MmsException Error occurred while moving the message.
1484     */
1485    public Uri move(Uri from, Uri to) throws MmsException {
1486        // Check whether the 'msgId' has been assigned a valid value.
1487        long msgId = ContentUris.parseId(from);
1488        if (msgId == -1L) {
1489            throw new MmsException("Error! ID of the message: -1.");
1490        }
1491
1492        // Get corresponding int value of destination box.
1493        Integer msgBox = MESSAGE_BOX_MAP.get(to);
1494        if (msgBox == null) {
1495            throw new MmsException(
1496                    "Bad destination, must be one of "
1497                    + "content://mms/inbox, content://mms/sent, "
1498                    + "content://mms/drafts, content://mms/outbox, "
1499                    + "content://mms/temp.");
1500        }
1501
1502        ContentValues values = new ContentValues(1);
1503        values.put(Mms.MESSAGE_BOX, msgBox);
1504        SqliteWrapper.update(mContext, mContentResolver, from, values, null, null);
1505        return ContentUris.withAppendedId(to, msgId);
1506    }
1507
1508    /**
1509     * Wrap a byte[] into a String.
1510     */
1511    public static String toIsoString(byte[] bytes) {
1512        try {
1513            return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
1514        } catch (UnsupportedEncodingException e) {
1515            // Impossible to reach here!
1516            Log.e(TAG, "ISO_8859_1 must be supported!", e);
1517            return "";
1518        }
1519    }
1520
1521    /**
1522     * Unpack a given String into a byte[].
1523     */
1524    public static byte[] getBytes(String data) {
1525        try {
1526            return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
1527        } catch (UnsupportedEncodingException e) {
1528            // Impossible to reach here!
1529            Log.e(TAG, "ISO_8859_1 must be supported!", e);
1530            return new byte[0];
1531        }
1532    }
1533
1534    /**
1535     * Remove all objects in the temporary path.
1536     */
1537    public void release() {
1538        Uri uri = Uri.parse(TEMPORARY_DRM_OBJECT_URI);
1539        SqliteWrapper.delete(mContext, mContentResolver, uri, null, null);
1540    }
1541
1542    /**
1543     * Find all messages to be sent or downloaded before certain time.
1544     */
1545    public Cursor getPendingMessages(long dueTime) {
1546        Uri.Builder uriBuilder = PendingMessages.CONTENT_URI.buildUpon();
1547        uriBuilder.appendQueryParameter("protocol", "mms");
1548
1549        String selection = PendingMessages.ERROR_TYPE + " < ?"
1550                + " AND " + PendingMessages.DUE_TIME + " <= ?";
1551
1552        String[] selectionArgs = new String[] {
1553                String.valueOf(MmsSms.ERR_TYPE_GENERIC_PERMANENT),
1554                String.valueOf(dueTime)
1555        };
1556
1557        return SqliteWrapper.query(mContext, mContentResolver,
1558                uriBuilder.build(), null, selection, selectionArgs,
1559                PendingMessages.DUE_TIME);
1560    }
1561}
1562