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