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