• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
RequestQueue(Looper looper)115         public RequestQueue(Looper looper) {
116             super(looper);
117         }
118 
119         @Override
handleMessage(Message msg)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 
getNetworkManager(int subId)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 
enforceSystemUid()150     private void enforceSystemUid() {
151         if (Binder.getCallingUid() != Process.SYSTEM_UID) {
152             throw new SecurityException("Only system can call this service");
153         }
154     }
155 
checkSubId(int subId)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
getCarrierMessagingServicePackageIfExists()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      */
startRequestQueueIfNeeded(int queueIndex)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
addSimRequest(MmsRequest request)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 
addToRunningRequestQueueSynchronized(MmsRequest request)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 
movePendingSimRequestsToRunningSynchronized()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
onBind(Intent intent)452     public IBinder onBind(Intent intent) {
453         return mStub;
454     }
455 
asBinder()456     public final IBinder asBinder() {
457         return mStub;
458     }
459 
460     @Override
onCreate()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 
importSms(String address, int type, String text, long timestampMillis, boolean seen, boolean read, String creator)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 
importMms(Uri contentUri, String messageId, long timestampSecs, boolean seen, boolean read, String creator)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 
isSmsMmsContentUri(Uri uri)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 
updateMessageStatus(Uri messageUri, ContentValues statusValues)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 + "=?";
archiveConversation(long conversationId, boolean archived)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 
addSmsDraft(String address, String text, String creator)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 
addMmsDraft(Uri contentUri, String creator)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      */
parsePduForAnyCarrier(final byte[] data)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
getAutoPersistingPref()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      */
readPduFromContentUri(final Uri contentUri, final int maxSize)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      */
writePduToContentUri(final Uri contentUri, final byte[] pdu)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