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