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
17package com.android.mms.service;
18
19import android.annotation.Nullable;
20import android.app.PendingIntent;
21import android.app.Service;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.database.sqlite.SQLiteException;
29import android.net.Uri;
30import android.os.Binder;
31import android.os.Bundle;
32import android.os.IBinder;
33import android.os.ParcelFileDescriptor;
34import android.os.Process;
35import android.os.RemoteException;
36import android.provider.Telephony;
37import android.service.carrier.CarrierMessagingService;
38import android.telephony.SmsManager;
39import android.telephony.SubscriptionManager;
40import android.telephony.TelephonyManager;
41import android.text.TextUtils;
42import android.util.SparseArray;
43
44import com.android.internal.telephony.IMms;
45import com.google.android.mms.MmsException;
46import com.google.android.mms.pdu.DeliveryInd;
47import com.google.android.mms.pdu.GenericPdu;
48import com.google.android.mms.pdu.NotificationInd;
49import com.google.android.mms.pdu.PduParser;
50import com.google.android.mms.pdu.PduPersister;
51import com.google.android.mms.pdu.ReadOrigInd;
52import com.google.android.mms.pdu.RetrieveConf;
53import com.google.android.mms.pdu.SendReq;
54import com.google.android.mms.util.SqliteWrapper;
55
56import java.io.IOException;
57import java.util.ArrayDeque;
58import java.util.Arrays;
59import java.util.List;
60import java.util.Queue;
61import java.util.concurrent.Callable;
62import java.util.concurrent.ExecutorService;
63import java.util.concurrent.Executors;
64import java.util.concurrent.Future;
65import java.util.concurrent.TimeUnit;
66
67/**
68 * System service to process MMS API requests
69 */
70public 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
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
124    private void enforceSystemUid() {
125        if (Binder.getCallingUid() != Process.SYSTEM_UID) {
126            throw new SecurityException("Only system can call this service");
127        }
128    }
129
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
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
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
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
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
450    public IBinder onBind(Intent intent) {
451        return mStub;
452    }
453
454    public final IBinder asBinder() {
455        return mStub;
456    }
457
458    @Override
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
475    public void onDestroy() {
476        super.onDestroy();
477        LogUtil.d("onDestroy");
478        for (ExecutorService executor : mRunningRequestExecutors) {
479            executor.shutdown();
480        }
481    }
482
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
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
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
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 + "=?";
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
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
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     */
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
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     */
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     */
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