Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mms.service;
     18 
     19 import com.google.android.mms.MmsException;
     20 import com.google.android.mms.pdu.DeliveryInd;
     21 import com.google.android.mms.pdu.GenericPdu;
     22 import com.google.android.mms.pdu.NotificationInd;
     23 import com.google.android.mms.pdu.PduComposer;
     24 import com.google.android.mms.pdu.PduParser;
     25 import com.google.android.mms.pdu.PduPersister;
     26 import com.google.android.mms.pdu.ReadOrigInd;
     27 import com.google.android.mms.pdu.RetrieveConf;
     28 import com.google.android.mms.pdu.SendReq;
     29 import com.google.android.mms.util.SqliteWrapper;
     30 
     31 import com.android.internal.telephony.IMms;
     32 import com.android.internal.telephony.SmsApplication;
     33 
     34 import android.app.Activity;
     35 import android.app.PendingIntent;
     36 import android.app.Service;
     37 import android.content.ContentResolver;
     38 import android.content.ContentUris;
     39 import android.content.ContentValues;
     40 import android.content.Context;
     41 import android.content.Intent;
     42 import android.content.SharedPreferences;
     43 import android.database.Cursor;
     44 import android.database.sqlite.SQLiteException;
     45 import android.net.Uri;
     46 import android.os.Binder;
     47 import android.os.Bundle;
     48 import android.os.Handler;
     49 import android.os.HandlerThread;
     50 import android.os.IBinder;
     51 import android.os.Looper;
     52 import android.os.ParcelFileDescriptor;
     53 import android.os.Process;
     54 import android.os.Message;
     55 import android.os.RemoteException;
     56 import android.provider.Telephony;
     57 import android.telephony.SmsManager;
     58 import android.text.TextUtils;
     59 import android.util.Log;
     60 
     61 import java.io.IOException;
     62 import java.util.Arrays;
     63 import java.util.concurrent.Callable;
     64 import java.util.concurrent.ConcurrentHashMap;
     65 import java.util.concurrent.ExecutorService;
     66 import java.util.concurrent.Executors;
     67 import java.util.concurrent.Future;
     68 import java.util.concurrent.TimeUnit;
     69 
     70 /**
     71  * System service to process MMS API requests
     72  */
     73 public class MmsService extends Service implements MmsRequest.RequestManager {
     74     public static final String TAG = "MmsService";
     75 
     76     public static final int QUEUE_INDEX_SEND = 0;
     77     public static final int QUEUE_INDEX_DOWNLOAD = 1;
     78 
     79     private static final String SHARED_PREFERENCES_NAME = "mmspref";
     80     private static final String PREF_AUTO_PERSISTING = "autopersisting";
     81 
     82     // Maximum time to spend waiting to read data from a content provider before failing with error.
     83     private static final int TASK_TIMEOUT_MS = 30 * 1000;
     84     // Maximum size of MMS service supports - used on occassions when MMS messages are processed
     85     // in a carrier independent manner (for example for imports and drafts) and the carrier
     86     // specific size limit should not be used (as it could be lower on some carriers).
     87     private static final int MAX_MMS_FILE_SIZE = 8 * 1024 * 1024;
     88 
     89     // Pending requests that are currently executed by carrier app
     90     // TODO: persist this in case MmsService crashes
     91     private final ConcurrentHashMap<Integer, MmsRequest> mPendingRequests =
     92             new ConcurrentHashMap<Integer, MmsRequest>();
     93 
     94     private final ExecutorService mExecutor = Executors.newCachedThreadPool();
     95 
     96     @Override
     97     public void addPending(int key, MmsRequest request) {
     98         mPendingRequests.put(key, request);
     99     }
    100 
    101     /**
    102      * A thread-based request queue for executing the MMS requests in serial order
    103      */
    104     private class RequestQueue extends Handler {
    105         public RequestQueue(Looper looper) {
    106             super(looper);
    107         }
    108 
    109         @Override
    110         public void handleMessage(Message msg) {
    111             final MmsRequest request = (MmsRequest) msg.obj;
    112             if (request != null) {
    113                 request.execute(MmsService.this, mMmsNetworkManager);
    114             }
    115         }
    116     }
    117 
    118     private void enforceSystemUid() {
    119         if (Binder.getCallingUid() != Process.SYSTEM_UID) {
    120             throw new SecurityException("Only system can call this service");
    121         }
    122     }
    123 
    124     private IMms.Stub mStub = new IMms.Stub() {
    125         @Override
    126         public void sendMessage(long subId, String callingPkg, Uri contentUri,
    127                 String locationUrl, Bundle configOverrides, PendingIntent sentIntent)
    128                         throws RemoteException {
    129             Log.d(TAG, "sendMessage");
    130             enforceSystemUid();
    131             final SendRequest request = new SendRequest(MmsService.this, subId, contentUri,
    132                     null/*messageUri*/, locationUrl, sentIntent, callingPkg, configOverrides);
    133             if (SmsApplication.shouldWriteMessageForPackage(callingPkg, MmsService.this)) {
    134                 // Store the message in outbox first before sending
    135                 request.storeInOutbox(MmsService.this);
    136             }
    137             // Try sending via carrier app
    138             request.trySendingByCarrierApp(MmsService.this);
    139         }
    140 
    141         @Override
    142         public void downloadMessage(long subId, String callingPkg, String locationUrl,
    143                 Uri contentUri, Bundle configOverrides,
    144                 PendingIntent downloadedIntent) throws RemoteException {
    145             Log.d(TAG, "downloadMessage: " + locationUrl);
    146             enforceSystemUid();
    147             final DownloadRequest request = new DownloadRequest(MmsService.this, subId,
    148                     locationUrl, contentUri, downloadedIntent, callingPkg, configOverrides);
    149             // Try downloading via carrier app
    150             request.tryDownloadingByCarrierApp(MmsService.this);
    151         }
    152 
    153         @Override
    154         public void updateMmsSendStatus(int messageRef, byte[] pdu, int status) {
    155             Log.d(TAG, "updateMmsSendStatus: ref=" + messageRef
    156                     + ", pdu=" + (pdu == null ? null : pdu.length) + ", status=" + status);
    157             enforceSystemUid();
    158             final MmsRequest request = mPendingRequests.get(messageRef);
    159             if (request != null) {
    160                 if (status != SmsManager.MMS_ERROR_RETRY) {
    161                     // Sent completed (maybe success or fail) by carrier app, finalize the request.
    162                     request.processResult(MmsService.this, status, pdu);
    163                 } else {
    164                     // Failed, try sending via carrier network
    165                     addRunning(request);
    166                 }
    167             } else {
    168                 // Really wrong here: can't find the request to update
    169                 Log.e(TAG, "Failed to find the request to update send status");
    170             }
    171         }
    172 
    173         @Override
    174         public void updateMmsDownloadStatus(int messageRef, int status) {
    175             Log.d(TAG, "updateMmsDownloadStatus: ref=" + messageRef + ", status=" + status);
    176             enforceSystemUid();
    177             final MmsRequest request = mPendingRequests.get(messageRef);
    178             if (request != null) {
    179                 if (status != SmsManager.MMS_ERROR_RETRY) {
    180                     // Downloaded completed (maybe success or fail) by carrier app, finalize the
    181                     // request.
    182                     request.processResult(MmsService.this, status, null/*response*/);
    183                 } else {
    184                     // Failed, try downloading via the carrier network
    185                     addRunning(request);
    186                 }
    187             } else {
    188                 // Really wrong here: can't find the request to update
    189                 Log.e(TAG, "Failed to find the request to update download status");
    190             }
    191         }
    192 
    193         @Override
    194         public Bundle getCarrierConfigValues(long subId) {
    195             Log.d(TAG, "getCarrierConfigValues");
    196             final MmsConfig mmsConfig = MmsConfigManager.getInstance().getMmsConfigBySubId(subId);
    197             if (mmsConfig == null) {
    198                 return new Bundle();
    199             }
    200             return mmsConfig.getCarrierConfigValues();
    201         }
    202 
    203         @Override
    204         public Uri importTextMessage(String callingPkg, String address, int type, String text,
    205                 long timestampMillis, boolean seen, boolean read) {
    206             Log.d(TAG, "importTextMessage");
    207             enforceSystemUid();
    208             return importSms(address, type, text, timestampMillis, seen, read, callingPkg);
    209         }
    210 
    211         @Override
    212         public Uri importMultimediaMessage(String callingPkg, Uri contentUri,
    213                 String messageId, long timestampSecs, boolean seen, boolean read) {
    214             Log.d(TAG, "importMultimediaMessage");
    215             enforceSystemUid();
    216             return importMms(contentUri, messageId, timestampSecs, seen, read, callingPkg);
    217         }
    218 
    219         @Override
    220         public boolean deleteStoredMessage(String callingPkg, Uri messageUri)
    221                 throws RemoteException {
    222             Log.d(TAG, "deleteStoredMessage " + messageUri);
    223             enforceSystemUid();
    224             if (!isSmsMmsContentUri(messageUri)) {
    225                 Log.e(TAG, "deleteStoredMessage: invalid message URI: " + messageUri.toString());
    226                 return false;
    227             }
    228             // Clear the calling identity and query the database using the phone user id
    229             // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    230             // between the calling uid and the package uid
    231             final long identity = Binder.clearCallingIdentity();
    232             try {
    233                 if (getContentResolver().delete(
    234                         messageUri, null/*where*/, null/*selectionArgs*/) != 1) {
    235                     Log.e(TAG, "deleteStoredMessage: failed to delete");
    236                     return false;
    237                 }
    238             } catch (SQLiteException e) {
    239                 Log.e(TAG, "deleteStoredMessage: failed to delete", e);
    240             } finally {
    241                 Binder.restoreCallingIdentity(identity);
    242             }
    243             return true;
    244         }
    245 
    246         @Override
    247         public boolean deleteStoredConversation(String callingPkg, long conversationId)
    248                 throws RemoteException {
    249             Log.d(TAG, "deleteStoredConversation " + conversationId);
    250             enforceSystemUid();
    251             if (conversationId == -1) {
    252                 Log.e(TAG, "deleteStoredConversation: invalid thread id");
    253                 return false;
    254             }
    255             final Uri uri = ContentUris.withAppendedId(
    256                     Telephony.Threads.CONTENT_URI, conversationId);
    257             // Clear the calling identity and query the database using the phone user id
    258             // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    259             // between the calling uid and the package uid
    260             final long identity = Binder.clearCallingIdentity();
    261             try {
    262                 if (getContentResolver().delete(uri, null, null) != 1) {
    263                     Log.e(TAG, "deleteStoredConversation: failed to delete");
    264                     return false;
    265                 }
    266             } catch (SQLiteException e) {
    267                 Log.e(TAG, "deleteStoredConversation: failed to delete", e);
    268             } finally {
    269                 Binder.restoreCallingIdentity(identity);
    270             }
    271             return true;
    272         }
    273 
    274         @Override
    275         public boolean updateStoredMessageStatus(String callingPkg, Uri messageUri,
    276                 ContentValues statusValues) throws RemoteException {
    277             Log.d(TAG, "updateStoredMessageStatus " + messageUri);
    278             enforceSystemUid();
    279             return updateMessageStatus(messageUri, statusValues);
    280         }
    281 
    282         @Override
    283         public boolean archiveStoredConversation(String callingPkg, long conversationId,
    284                 boolean archived) throws RemoteException {
    285             Log.d(TAG, "archiveStoredConversation " + conversationId + " " + archived);
    286             if (conversationId == -1) {
    287                 Log.e(TAG, "archiveStoredConversation: invalid thread id");
    288                 return false;
    289             }
    290             return archiveConversation(conversationId, archived);
    291         }
    292 
    293         @Override
    294         public Uri addTextMessageDraft(String callingPkg, String address, String text)
    295                 throws RemoteException {
    296             Log.d(TAG, "addTextMessageDraft");
    297             enforceSystemUid();
    298             return addSmsDraft(address, text, callingPkg);
    299         }
    300 
    301         @Override
    302         public Uri addMultimediaMessageDraft(String callingPkg, Uri contentUri)
    303                 throws RemoteException {
    304             Log.d(TAG, "addMultimediaMessageDraft");
    305             enforceSystemUid();
    306             return addMmsDraft(contentUri, callingPkg);
    307         }
    308 
    309         @Override
    310         public void sendStoredMessage(long subId, String callingPkg, Uri messageUri,
    311                 Bundle configOverrides, PendingIntent sentIntent) throws RemoteException {
    312             throw new UnsupportedOperationException();
    313         }
    314 
    315         @Override
    316         public void setAutoPersisting(String callingPkg, boolean enabled) throws RemoteException {
    317             Log.d(TAG, "setAutoPersisting " + enabled);
    318             enforceSystemUid();
    319             final SharedPreferences preferences = getSharedPreferences(
    320                     SHARED_PREFERENCES_NAME, MODE_PRIVATE);
    321             final SharedPreferences.Editor editor = preferences.edit();
    322             editor.putBoolean(PREF_AUTO_PERSISTING, enabled);
    323             editor.apply();
    324         }
    325 
    326         @Override
    327         public boolean getAutoPersisting() throws RemoteException {
    328             Log.d(TAG, "getAutoPersisting");
    329             return getAutoPersistingPref();
    330         }
    331     };
    332 
    333     // Request queue threads
    334     // 0: send queue
    335     // 1: download queue
    336     private final RequestQueue[] mRequestQueues = new RequestQueue[2];
    337 
    338     // Manages MMS connectivity related stuff
    339     private final MmsNetworkManager mMmsNetworkManager = new MmsNetworkManager(this);
    340 
    341     /**
    342      * Lazy start the request queue threads
    343      *
    344      * @param queueIndex index of the queue to start
    345      */
    346     private void startRequestQueueIfNeeded(int queueIndex) {
    347         if (queueIndex < 0 || queueIndex >= mRequestQueues.length) {
    348             return;
    349         }
    350         synchronized (this) {
    351             if (mRequestQueues[queueIndex] == null) {
    352                 final HandlerThread thread =
    353                         new HandlerThread("MmsService RequestQueue " + queueIndex);
    354                 thread.start();
    355                 mRequestQueues[queueIndex] = new RequestQueue(thread.getLooper());
    356             }
    357         }
    358     }
    359 
    360     @Override
    361     public void addRunning(MmsRequest request) {
    362         if (request == null) {
    363             return;
    364         }
    365         final int queue = request.getRunningQueue();
    366         startRequestQueueIfNeeded(queue);
    367         final Message message = Message.obtain();
    368         message.obj = request;
    369         mRequestQueues[queue].sendMessage(message);
    370     }
    371 
    372     @Override
    373     public IBinder onBind(Intent intent) {
    374         return mStub;
    375     }
    376 
    377     public final IBinder asBinder() {
    378         return mStub;
    379     }
    380 
    381     @Override
    382     public void onCreate() {
    383         super.onCreate();
    384         Log.d(TAG, "onCreate");
    385         // Load mms_config
    386         // TODO (ywen): make sure we start request queues after mms_config is loaded
    387         MmsConfigManager.getInstance().init(this);
    388     }
    389 
    390     private Uri importSms(String address, int type, String text, long timestampMillis,
    391             boolean seen, boolean read, String creator) {
    392         Uri insertUri = null;
    393         switch (type) {
    394             case SmsManager.SMS_TYPE_INCOMING:
    395                 insertUri = Telephony.Sms.Inbox.CONTENT_URI;
    396 
    397                 break;
    398             case SmsManager.SMS_TYPE_OUTGOING:
    399                 insertUri = Telephony.Sms.Sent.CONTENT_URI;
    400                 break;
    401         }
    402         if (insertUri == null) {
    403             Log.e(TAG, "importTextMessage: invalid message type for importing: " + type);
    404             return null;
    405         }
    406         final ContentValues values = new ContentValues(6);
    407         values.put(Telephony.Sms.ADDRESS, address);
    408         values.put(Telephony.Sms.DATE, timestampMillis);
    409         values.put(Telephony.Sms.SEEN, seen ? 1 : 0);
    410         values.put(Telephony.Sms.READ, read ? 1 : 0);
    411         values.put(Telephony.Sms.BODY, text);
    412         if (!TextUtils.isEmpty(creator)) {
    413             values.put(Telephony.Mms.CREATOR, creator);
    414         }
    415         // Clear the calling identity and query the database using the phone user id
    416         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    417         // between the calling uid and the package uid
    418         final long identity = Binder.clearCallingIdentity();
    419         try {
    420             return getContentResolver().insert(insertUri, values);
    421         } catch (SQLiteException e) {
    422             Log.e(TAG, "importTextMessage: failed to persist imported text message", e);
    423         } finally {
    424             Binder.restoreCallingIdentity(identity);
    425         }
    426         return null;
    427     }
    428 
    429     private Uri importMms(Uri contentUri, String messageId, long timestampSecs,
    430             boolean seen, boolean read, String creator) {
    431         byte[] pduData = readPduFromContentUri(contentUri, MAX_MMS_FILE_SIZE);
    432         if (pduData == null || pduData.length < 1) {
    433             Log.e(TAG, "importMessage: empty PDU");
    434             return null;
    435         }
    436         // Clear the calling identity and query the database using the phone user id
    437         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    438         // between the calling uid and the package uid
    439         final long identity = Binder.clearCallingIdentity();
    440         try {
    441             final GenericPdu pdu = (new PduParser(pduData)).parse();
    442             if (pdu == null) {
    443                 Log.e(TAG, "importMessage: can't parse input PDU");
    444                 return null;
    445             }
    446             Uri insertUri = null;
    447             if (pdu instanceof SendReq) {
    448                 insertUri = Telephony.Mms.Sent.CONTENT_URI;
    449             } else if (pdu instanceof RetrieveConf ||
    450                     pdu instanceof NotificationInd ||
    451                     pdu instanceof DeliveryInd ||
    452                     pdu instanceof ReadOrigInd) {
    453                 insertUri = Telephony.Mms.Inbox.CONTENT_URI;
    454             }
    455             if (insertUri == null) {
    456                 Log.e(TAG, "importMessage; invalid MMS type: " + pdu.getClass().getCanonicalName());
    457                 return null;
    458             }
    459             final PduPersister persister = PduPersister.getPduPersister(this);
    460             final Uri uri = persister.persist(
    461                     pdu,
    462                     insertUri,
    463                     true/*createThreadId*/,
    464                     true/*groupMmsEnabled*/,
    465                     null/*preOpenedFiles*/);
    466             if (uri == null) {
    467                 Log.e(TAG, "importMessage: failed to persist message");
    468                 return null;
    469             }
    470             final ContentValues values = new ContentValues(5);
    471             if (!TextUtils.isEmpty(messageId)) {
    472                 values.put(Telephony.Mms.MESSAGE_ID, messageId);
    473             }
    474             if (timestampSecs != -1) {
    475                 values.put(Telephony.Mms.DATE, timestampSecs);
    476             }
    477             values.put(Telephony.Mms.READ, seen ? 1 : 0);
    478             values.put(Telephony.Mms.SEEN, read ? 1 : 0);
    479             if (!TextUtils.isEmpty(creator)) {
    480                 values.put(Telephony.Mms.CREATOR, creator);
    481             }
    482             if (SqliteWrapper.update(this, getContentResolver(), uri, values,
    483                     null/*where*/, null/*selectionArg*/) != 1) {
    484                 Log.e(TAG, "importMessage: failed to update message");
    485             }
    486             return uri;
    487         } catch (RuntimeException e) {
    488             Log.e(TAG, "importMessage: failed to parse input PDU", e);
    489         } catch (MmsException e) {
    490             Log.e(TAG, "importMessage: failed to persist message", e);
    491         } finally {
    492             Binder.restoreCallingIdentity(identity);
    493         }
    494         return null;
    495     }
    496 
    497     private static boolean isSmsMmsContentUri(Uri uri) {
    498         final String uriString = uri.toString();
    499         if (!uriString.startsWith("content://sms/") && !uriString.startsWith("content://mms/")) {
    500             return false;
    501         }
    502         if (ContentUris.parseId(uri) == -1) {
    503             return false;
    504         }
    505         return true;
    506     }
    507 
    508     private boolean updateMessageStatus(Uri messageUri, ContentValues statusValues) {
    509         if (!isSmsMmsContentUri(messageUri)) {
    510             Log.e(TAG, "updateMessageStatus: invalid messageUri: " + messageUri.toString());
    511             return false;
    512         }
    513         if (statusValues == null) {
    514             Log.w(TAG, "updateMessageStatus: empty values to update");
    515             return false;
    516         }
    517         final ContentValues values = new ContentValues();
    518         if (statusValues.containsKey(SmsManager.MESSAGE_STATUS_READ)) {
    519             final Integer val = statusValues.getAsInteger(SmsManager.MESSAGE_STATUS_READ);
    520             if (val != null) {
    521                 // MMS uses the same column name
    522                 values.put(Telephony.Sms.READ, val);
    523             }
    524         } else if (statusValues.containsKey(SmsManager.MESSAGE_STATUS_SEEN)) {
    525             final Integer val = statusValues.getAsInteger(SmsManager.MESSAGE_STATUS_SEEN);
    526             if (val != null) {
    527                 // MMS uses the same column name
    528                 values.put(Telephony.Sms.SEEN, val);
    529             }
    530         }
    531         if (values.size() < 1) {
    532             Log.w(TAG, "updateMessageStatus: no value to update");
    533             return false;
    534         }
    535         // Clear the calling identity and query the database using the phone user id
    536         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    537         // between the calling uid and the package uid
    538         final long identity = Binder.clearCallingIdentity();
    539         try {
    540             if (getContentResolver().update(
    541                     messageUri, values, null/*where*/, null/*selectionArgs*/) != 1) {
    542                 Log.e(TAG, "updateMessageStatus: failed to update database");
    543                 return false;
    544             }
    545             return true;
    546         } catch (SQLiteException e) {
    547             Log.e(TAG, "updateMessageStatus: failed to update database", e);
    548         } finally {
    549             Binder.restoreCallingIdentity(identity);
    550         }
    551         return false;
    552     }
    553 
    554     private static final String ARCHIVE_CONVERSATION_SELECTION = Telephony.Threads._ID + "=?";
    555     private boolean archiveConversation(long conversationId, boolean archived) {
    556         final ContentValues values = new ContentValues(1);
    557         values.put(Telephony.Threads.ARCHIVED, archived ? 1 : 0);
    558         // Clear the calling identity and query the database using the phone user id
    559         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    560         // between the calling uid and the package uid
    561         final long identity = Binder.clearCallingIdentity();
    562         try {
    563             if (getContentResolver().update(
    564                     Telephony.Threads.CONTENT_URI,
    565                     values,
    566                     ARCHIVE_CONVERSATION_SELECTION,
    567                     new String[] { Long.toString(conversationId)}) != 1) {
    568                 Log.e(TAG, "archiveConversation: failed to update database");
    569                 return false;
    570             }
    571             return true;
    572         } catch (SQLiteException e) {
    573             Log.e(TAG, "archiveConversation: failed to update database", e);
    574         } finally {
    575             Binder.restoreCallingIdentity(identity);
    576         }
    577         return false;
    578     }
    579 
    580     private Uri addSmsDraft(String address, String text, String creator) {
    581         final ContentValues values = new ContentValues(5);
    582         values.put(Telephony.Sms.ADDRESS, address);
    583         values.put(Telephony.Sms.BODY, text);
    584         values.put(Telephony.Sms.READ, 1);
    585         values.put(Telephony.Sms.SEEN, 1);
    586         if (!TextUtils.isEmpty(creator)) {
    587             values.put(Telephony.Mms.CREATOR, creator);
    588         }
    589         // Clear the calling identity and query the database using the phone user id
    590         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    591         // between the calling uid and the package uid
    592         final long identity = Binder.clearCallingIdentity();
    593         try {
    594             return getContentResolver().insert(Telephony.Sms.Draft.CONTENT_URI, values);
    595         } catch (SQLiteException e) {
    596             Log.e(TAG, "addSmsDraft: failed to store draft message", e);
    597         } finally {
    598             Binder.restoreCallingIdentity(identity);
    599         }
    600         return null;
    601     }
    602 
    603     private Uri addMmsDraft(Uri contentUri, String creator) {
    604         byte[] pduData = readPduFromContentUri(contentUri, MAX_MMS_FILE_SIZE);
    605         if (pduData == null || pduData.length < 1) {
    606             Log.e(TAG, "addMmsDraft: empty PDU");
    607             return null;
    608         }
    609         // Clear the calling identity and query the database using the phone user id
    610         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    611         // between the calling uid and the package uid
    612         final long identity = Binder.clearCallingIdentity();
    613         try {
    614             final GenericPdu pdu = (new PduParser(pduData)).parse();
    615             if (pdu == null) {
    616                 Log.e(TAG, "addMmsDraft: can't parse input PDU");
    617                 return null;
    618             }
    619             if (!(pdu instanceof SendReq)) {
    620                 Log.e(TAG, "addMmsDraft; invalid MMS type: " + pdu.getClass().getCanonicalName());
    621                 return null;
    622             }
    623             final PduPersister persister = PduPersister.getPduPersister(this);
    624             final Uri uri = persister.persist(
    625                     pdu,
    626                     Telephony.Mms.Draft.CONTENT_URI,
    627                     true/*createThreadId*/,
    628                     true/*groupMmsEnabled*/,
    629                     null/*preOpenedFiles*/);
    630             if (uri == null) {
    631                 Log.e(TAG, "addMmsDraft: failed to persist message");
    632                 return null;
    633             }
    634             final ContentValues values = new ContentValues(3);
    635             values.put(Telephony.Mms.READ, 1);
    636             values.put(Telephony.Mms.SEEN, 1);
    637             if (!TextUtils.isEmpty(creator)) {
    638                 values.put(Telephony.Mms.CREATOR, creator);
    639             }
    640             if (SqliteWrapper.update(this, getContentResolver(), uri, values,
    641                     null/*where*/, null/*selectionArg*/) != 1) {
    642                 Log.e(TAG, "addMmsDraft: failed to update message");
    643             }
    644             return uri;
    645         } catch (RuntimeException e) {
    646             Log.e(TAG, "addMmsDraft: failed to parse input PDU", e);
    647         } catch (MmsException e) {
    648             Log.e(TAG, "addMmsDraft: failed to persist message", e);
    649         } finally {
    650             Binder.restoreCallingIdentity(identity);
    651         }
    652         return null;
    653     }
    654 
    655     private boolean isFailedOrDraft(Uri messageUri) {
    656         // Clear the calling identity and query the database using the phone user id
    657         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    658         // between the calling uid and the package uid
    659         final long identity = Binder.clearCallingIdentity();
    660         Cursor cursor = null;
    661         try {
    662             cursor = getContentResolver().query(
    663                     messageUri,
    664                     new String[]{ Telephony.Mms.MESSAGE_BOX },
    665                     null/*selection*/,
    666                     null/*selectionArgs*/,
    667                     null/*sortOrder*/);
    668             if (cursor != null && cursor.moveToFirst()) {
    669                 final int box = cursor.getInt(0);
    670                 return box == Telephony.Mms.MESSAGE_BOX_DRAFTS
    671                         || box == Telephony.Mms.MESSAGE_BOX_FAILED;
    672             }
    673         } catch (SQLiteException e) {
    674             Log.e(TAG, "isFailedOrDraft: query message type failed", e);
    675         } finally {
    676             if (cursor != null) {
    677                 cursor.close();
    678             }
    679             Binder.restoreCallingIdentity(identity);
    680         }
    681         return false;
    682     }
    683 
    684     private byte[] loadPdu(Uri messageUri) {
    685         // Clear the calling identity and query the database using the phone user id
    686         // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
    687         // between the calling uid and the package uid
    688         final long identity = Binder.clearCallingIdentity();
    689         try {
    690             final PduPersister persister = PduPersister.getPduPersister(this);
    691             final GenericPdu pdu = persister.load(messageUri);
    692             if (pdu == null) {
    693                 Log.e(TAG, "loadPdu: failed to load PDU from " + messageUri.toString());
    694                 return null;
    695             }
    696             final PduComposer composer = new PduComposer(this, pdu);
    697             return composer.make();
    698         } catch (MmsException e) {
    699             Log.e(TAG, "loadPdu: failed to load PDU from " + messageUri.toString(), e);
    700         } catch (RuntimeException e) {
    701             Log.e(TAG, "loadPdu: failed to serialize PDU", e);
    702         } finally {
    703             Binder.restoreCallingIdentity(identity);
    704         }
    705         return null;
    706     }
    707 
    708     private void returnUnspecifiedFailure(PendingIntent pi) {
    709         if (pi != null) {
    710             try {
    711                 pi.send(SmsManager.MMS_ERROR_UNSPECIFIED);
    712             } catch (PendingIntent.CanceledException e) {
    713                 // ignore
    714             }
    715         }
    716     }
    717 
    718     @Override
    719     public boolean getAutoPersistingPref() {
    720         final SharedPreferences preferences = getSharedPreferences(
    721                 SHARED_PREFERENCES_NAME, MODE_PRIVATE);
    722         return preferences.getBoolean(PREF_AUTO_PERSISTING, false);
    723     }
    724 
    725     /**
    726      * Read pdu from content provider uri
    727      * @param contentUri content provider uri from which to read
    728      * @param maxSize maximum number of bytes to read
    729      * @return pdu bytes if succeeded else null
    730      */
    731     public byte[] readPduFromContentUri(final Uri contentUri, final int maxSize) {
    732         Callable<byte[]> copyPduToArray = new Callable<byte[]>() {
    733             public byte[] call() {
    734                 ParcelFileDescriptor.AutoCloseInputStream inStream = null;
    735                 try {
    736                     ContentResolver cr = MmsService.this.getContentResolver();
    737                     ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "r");
    738                     inStream = new ParcelFileDescriptor.AutoCloseInputStream(pduFd);
    739                     // Request one extra byte to make sure file not bigger than maxSize
    740                     byte[] tempBody = new byte[maxSize+1];
    741                     int bytesRead = inStream.read(tempBody, 0, maxSize+1);
    742                     if (bytesRead == 0) {
    743                         Log.e(MmsService.TAG, "MmsService.readPduFromContentUri: empty PDU");
    744                         return null;
    745                     }
    746                     if (bytesRead <= maxSize) {
    747                         return Arrays.copyOf(tempBody, bytesRead);
    748                     }
    749                     Log.e(MmsService.TAG, "MmsService.readPduFromContentUri: PDU too large");
    750                     return null;
    751                 } catch (IOException ex) {
    752                     Log.e(MmsService.TAG,
    753                             "MmsService.readPduFromContentUri: IO exception reading PDU", ex);
    754                     return null;
    755                 } finally {
    756                     if (inStream != null) {
    757                         try {
    758                             inStream.close();
    759                         } catch (IOException ex) {
    760                         }
    761                     }
    762                 }
    763             }
    764         };
    765 
    766         Future<byte[]> pendingResult = mExecutor.submit(copyPduToArray);
    767         try {
    768             byte[] pdu = pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    769             return pdu;
    770         } catch (Exception e) {
    771             // Typically a timeout occurred - cancel task
    772             pendingResult.cancel(true);
    773         }
    774         return null;
    775     }
    776 
    777     /**
    778      * Write pdu bytes to content provider uri
    779      * @param contentUri content provider uri to which bytes should be written
    780      * @param pdu Bytes to write
    781      * @return true if all bytes successfully written else false
    782      */
    783     public boolean writePduToContentUri(final Uri contentUri, final byte[] pdu) {
    784         Callable<Boolean> copyDownloadedPduToOutput = new Callable<Boolean>() {
    785             public Boolean call() {
    786                 ParcelFileDescriptor.AutoCloseOutputStream outStream = null;
    787                 try {
    788                     ContentResolver cr = MmsService.this.getContentResolver();
    789                     ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "w");
    790                     outStream = new ParcelFileDescriptor.AutoCloseOutputStream(pduFd);
    791                     outStream.write(pdu);
    792                     return Boolean.TRUE;
    793                 } catch (IOException ex) {
    794                     return Boolean.FALSE;
    795                 } finally {
    796                     if (outStream != null) {
    797                         try {
    798                             outStream.close();
    799                         } catch (IOException ex) {
    800                         }
    801                     }
    802                 }
    803             }
    804         };
    805 
    806         Future<Boolean> pendingResult = mExecutor.submit(copyDownloadedPduToOutput);
    807         try {
    808             Boolean succeeded = pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    809             return succeeded == Boolean.TRUE;
    810         } catch (Exception e) {
    811             // Typically a timeout occurred - cancel task
    812             pendingResult.cancel(true);
    813         }
    814         return false;
    815     }
    816 }
    817