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