Home | History | Annotate | Download | only in pdu
      1 /*
      2  * Copyright (C) 2007-2008 Esmertec AG.
      3  * Copyright (C) 2007-2008 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.google.android.mms.pdu;
     19 
     20 import com.google.android.mms.ContentType;
     21 import com.google.android.mms.InvalidHeaderValueException;
     22 import com.google.android.mms.MmsException;
     23 import com.google.android.mms.util.DownloadDrmHelper;
     24 import com.google.android.mms.util.DrmConvertSession;
     25 import com.google.android.mms.util.PduCache;
     26 import com.google.android.mms.util.PduCacheEntry;
     27 import com.google.android.mms.util.SqliteWrapper;
     28 
     29 import android.content.ContentResolver;
     30 import android.content.ContentUris;
     31 import android.content.ContentValues;
     32 import android.content.Context;
     33 import android.database.Cursor;
     34 import android.database.DatabaseUtils;
     35 import android.database.sqlite.SQLiteException;
     36 import android.drm.DrmManagerClient;
     37 import android.net.Uri;
     38 import android.provider.MediaStore;
     39 import android.provider.Telephony;
     40 import android.provider.Telephony.Mms;
     41 import android.provider.Telephony.MmsSms;
     42 import android.provider.Telephony.Threads;
     43 import android.provider.Telephony.Mms.Addr;
     44 import android.provider.Telephony.Mms.Part;
     45 import android.provider.Telephony.MmsSms.PendingMessages;
     46 import android.telephony.PhoneNumberUtils;
     47 import android.telephony.TelephonyManager;
     48 import android.text.TextUtils;
     49 import android.util.Log;
     50 
     51 import java.io.ByteArrayOutputStream;
     52 import java.io.File;
     53 import java.io.FileNotFoundException;
     54 import java.io.IOException;
     55 import java.io.InputStream;
     56 import java.io.OutputStream;
     57 import java.io.UnsupportedEncodingException;
     58 import java.util.ArrayList;
     59 import java.util.HashMap;
     60 import java.util.HashSet;
     61 import java.util.Map;
     62 import java.util.Set;
     63 import java.util.Map.Entry;
     64 
     65 import com.google.android.mms.pdu.EncodedStringValue;
     66 
     67 /**
     68  * This class is the high-level manager of PDU storage.
     69  */
     70 public class PduPersister {
     71     private static final String TAG = "PduPersister";
     72     private static final boolean 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