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