| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mms.service; |
| |
| import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE; |
| import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_SEND_REQ; |
| |
| import android.annotation.Nullable; |
| import android.app.PendingIntent; |
| import android.app.Service; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.database.sqlite.SQLiteException; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.ParcelFileDescriptor; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.provider.Telephony; |
| import android.service.carrier.CarrierMessagingService; |
| import android.telephony.SmsManager; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.TelephonyManager; |
| import android.telephony.data.ApnSetting; |
| import android.text.TextUtils; |
| import android.util.SparseArray; |
| |
| import com.android.internal.telephony.IMms; |
| |
| import com.google.android.mms.MmsException; |
| import com.google.android.mms.pdu.DeliveryInd; |
| import com.google.android.mms.pdu.GenericPdu; |
| import com.google.android.mms.pdu.NotificationInd; |
| import com.google.android.mms.pdu.PduParser; |
| import com.google.android.mms.pdu.PduPersister; |
| import com.google.android.mms.pdu.ReadOrigInd; |
| import com.google.android.mms.pdu.RetrieveConf; |
| import com.google.android.mms.pdu.SendReq; |
| import com.google.android.mms.util.SqliteWrapper; |
| |
| import java.io.IOException; |
| import java.util.ArrayDeque; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * System service to process MMS API requests |
| */ |
| public class MmsService extends Service implements MmsRequest.RequestManager { |
| public static final int QUEUE_INDEX_SEND = 0; |
| public static final int QUEUE_INDEX_DOWNLOAD = 1; |
| |
| private static final String SHARED_PREFERENCES_NAME = "mmspref"; |
| private static final String PREF_AUTO_PERSISTING = "autopersisting"; |
| |
| // Maximum time to spend waiting to read data from a content provider before failing with error. |
| private static final int TASK_TIMEOUT_MS = 30 * 1000; |
| // Maximum size of MMS service supports - used on occassions when MMS messages are processed |
| // in a carrier independent manner (for example for imports and drafts) and the carrier |
| // specific size limit should not be used (as it could be lower on some carriers). |
| private static final int MAX_MMS_FILE_SIZE = 8 * 1024 * 1024; |
| |
| // The default number of threads allowed to run MMS requests in each queue |
| public static final int THREAD_POOL_SIZE = 4; |
| |
| /** Represents the received SMS message for importing. */ |
| public static final int SMS_TYPE_INCOMING = 0; |
| /** Represents the sent SMS message for importing. */ |
| public static final int SMS_TYPE_OUTGOING = 1; |
| /** Message status property: whether the message has been seen. */ |
| public static final String MESSAGE_STATUS_SEEN = "seen"; |
| /** Message status property: whether the message has been read. */ |
| public static final String MESSAGE_STATUS_READ = "read"; |
| |
| // Pending requests that are waiting for the SIM to be available |
| // If a different SIM is currently used by previous requests, the following |
| // requests will stay in this queue until that SIM finishes its current requests in |
| // RequestQueue. |
| // Requests are not reordered. So, e.g. if current SIM is SIM1, a request for SIM2 will be |
| // blocked in the queue. And a later request for SIM1 will be appended to the queue, ordered |
| // after the request for SIM2, instead of being put into the running queue. |
| // TODO: persist this in case MmsService crashes |
| private final Queue<MmsRequest> mPendingSimRequestQueue = new ArrayDeque<>(); |
| |
| // Thread pool for transferring PDU with MMS apps |
| private final ExecutorService mPduTransferExecutor = Executors.newCachedThreadPool(); |
| |
| // A cache of MmsNetworkManager for SIMs |
| private final SparseArray<MmsNetworkManager> mNetworkManagerCache = new SparseArray<>(); |
| |
| // The default TelephonyManager and a cache of TelephonyManagers for individual subscriptions |
| private TelephonyManager mDefaultTelephonyManager; |
| private final SparseArray<TelephonyManager> mTelephonyManagerCache = new SparseArray<>(); |
| |
| // The current SIM ID for the running requests. Only one SIM can send/download MMS at a time. |
| private int mCurrentSubId; |
| // The current running MmsRequest count. |
| private int mRunningRequestCount; |
| |
| // Running request queues, one thread pool per queue |
| // 0: send queue |
| // 1: download queue |
| private final ExecutorService[] mRunningRequestExecutors = new ExecutorService[2]; |
| |
| private MmsNetworkManager getNetworkManager(int subId) { |
| synchronized (mNetworkManagerCache) { |
| MmsNetworkManager manager = mNetworkManagerCache.get(subId); |
| if (manager == null) { |
| manager = new MmsNetworkManager(this, subId); |
| mNetworkManagerCache.put(subId, manager); |
| } |
| return manager; |
| } |
| } |
| |
| private TelephonyManager getTelephonyManager(int subId) { |
| synchronized (mTelephonyManagerCache) { |
| if (mDefaultTelephonyManager == null) { |
| mDefaultTelephonyManager = (TelephonyManager) this.getSystemService( |
| Context.TELEPHONY_SERVICE); |
| } |
| |
| TelephonyManager subSpecificTelephonyManager = mTelephonyManagerCache.get(subId); |
| if (subSpecificTelephonyManager == null) { |
| subSpecificTelephonyManager = mDefaultTelephonyManager.createForSubscriptionId( |
| subId); |
| mTelephonyManagerCache.put(subId, subSpecificTelephonyManager); |
| } |
| return subSpecificTelephonyManager; |
| } |
| } |
| |
| private void enforceSystemUid() { |
| if (Binder.getCallingUid() != Process.SYSTEM_UID) { |
| throw new SecurityException("Only system can call this service"); |
| } |
| } |
| |
| @Nullable |
| private String getCarrierMessagingServicePackageIfExists(int subId) { |
| Intent intent = new Intent(CarrierMessagingService.SERVICE_INTERFACE); |
| TelephonyManager telephonyManager = getTelephonyManager(subId); |
| List<String> carrierPackages = telephonyManager.getCarrierPackageNamesForIntent(intent); |
| |
| if (carrierPackages == null || carrierPackages.size() != 1) { |
| return null; |
| } else { |
| return carrierPackages.get(0); |
| } |
| } |
| |
| private IMms.Stub mStub = new IMms.Stub() { |
| @Override |
| public void sendMessage(int subId, String callingPkg, Uri contentUri, |
| String locationUrl, Bundle configOverrides, PendingIntent sentIntent) { |
| LogUtil.d("sendMessage"); |
| enforceSystemUid(); |
| |
| // Make sure the subId is correct |
| if (!SubscriptionManager.isValidSubscriptionId(subId)) { |
| LogUtil.e("Invalid subId " + subId); |
| sendErrorInPendingIntent(sentIntent); |
| return; |
| } |
| if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { |
| subId = SubscriptionManager.getDefaultSmsSubscriptionId(); |
| } |
| |
| // Make sure the subId is active |
| if (!isActiveSubId(subId)) { |
| sendErrorInPendingIntent(sentIntent); |
| return; |
| } |
| |
| final SendRequest request = new SendRequest(MmsService.this, subId, contentUri, |
| locationUrl, sentIntent, callingPkg, configOverrides, MmsService.this); |
| |
| final String carrierMessagingServicePackage = |
| getCarrierMessagingServicePackageIfExists(subId); |
| |
| if (carrierMessagingServicePackage != null) { |
| LogUtil.d(request.toString(), "sending message by carrier app"); |
| request.trySendingByCarrierApp(MmsService.this, carrierMessagingServicePackage); |
| return; |
| } |
| |
| // Make sure subId has MMS data. We intentionally do this after attempting to send via a |
| // carrier messaging service as the carrier messaging service may want to handle this in |
| // a different way and may not be restricted by whether data is enabled for an APN on a |
| // given subscription. |
| if (!getTelephonyManager(subId).isDataEnabledForApn(ApnSetting.TYPE_MMS)) { |
| // ENABLE_MMS_DATA_REQUEST_REASON_OUTGOING_MMS is set for only SendReq case, since |
| // AcknowledgeInd and NotifyRespInd are parts of downloading sequence. |
| // TODO: Should consider ReadRecInd(Read Report)? |
| sendSettingsIntentForFailedMms(!isRawPduSendReq(contentUri), subId); |
| sendErrorInPendingIntent(sentIntent); |
| return; |
| } |
| |
| addSimRequest(request); |
| } |
| |
| @Override |
| public void downloadMessage(int subId, String callingPkg, String locationUrl, |
| Uri contentUri, Bundle configOverrides, |
| PendingIntent downloadedIntent) { |
| // If the subId is no longer active it could be caused by an MVNO using multiple |
| // subIds, so we should try to download anyway. |
| // TODO: Fail fast when downloading will fail (i.e. SIM swapped) |
| LogUtil.d("downloadMessage: " + MmsHttpClient.redactUrlForNonVerbose(locationUrl)); |
| |
| enforceSystemUid(); |
| |
| // Make sure the subId is correct |
| if (!SubscriptionManager.isValidSubscriptionId(subId)) { |
| LogUtil.e("Invalid subId " + subId); |
| sendErrorInPendingIntent(downloadedIntent); |
| return; |
| } |
| if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { |
| subId = SubscriptionManager.getDefaultSmsSubscriptionId(); |
| } |
| |
| final DownloadRequest request = new DownloadRequest(MmsService.this, subId, locationUrl, |
| contentUri, downloadedIntent, callingPkg, configOverrides, MmsService.this); |
| |
| final String carrierMessagingServicePackage = |
| getCarrierMessagingServicePackageIfExists(subId); |
| |
| if (carrierMessagingServicePackage != null) { |
| LogUtil.d(request.toString(), "downloading message by carrier app"); |
| request.tryDownloadingByCarrierApp(MmsService.this, carrierMessagingServicePackage); |
| return; |
| } |
| |
| // Make sure subId has MMS data |
| if (!getTelephonyManager(subId).isDataEnabledForApn(ApnSetting.TYPE_MMS)) { |
| sendSettingsIntentForFailedMms(/*isIncoming=*/ true, subId); |
| sendErrorInPendingIntent(downloadedIntent); |
| return; |
| } |
| |
| addSimRequest(request); |
| } |
| |
| @Override |
| public Uri importTextMessage(String callingPkg, String address, int type, String text, |
| long timestampMillis, boolean seen, boolean read) { |
| LogUtil.d("importTextMessage"); |
| enforceSystemUid(); |
| return importSms(address, type, text, timestampMillis, seen, read, callingPkg); |
| } |
| |
| @Override |
| public Uri importMultimediaMessage(String callingPkg, Uri contentUri, |
| String messageId, long timestampSecs, boolean seen, boolean read) { |
| LogUtil.d("importMultimediaMessage"); |
| enforceSystemUid(); |
| return importMms(contentUri, messageId, timestampSecs, seen, read, callingPkg); |
| } |
| |
| @Override |
| public boolean deleteStoredMessage(String callingPkg, Uri messageUri) |
| throws RemoteException { |
| LogUtil.d("deleteStoredMessage " + messageUri); |
| enforceSystemUid(); |
| if (!isSmsMmsContentUri(messageUri)) { |
| LogUtil.e("deleteStoredMessage: invalid message URI: " + messageUri.toString()); |
| return false; |
| } |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| if (getContentResolver().delete( |
| messageUri, null/*where*/, null/*selectionArgs*/) != 1) { |
| LogUtil.e("deleteStoredMessage: failed to delete"); |
| return false; |
| } |
| } catch (SQLiteException e) { |
| LogUtil.e("deleteStoredMessage: failed to delete", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean deleteStoredConversation(String callingPkg, long conversationId) |
| throws RemoteException { |
| LogUtil.d("deleteStoredConversation " + conversationId); |
| enforceSystemUid(); |
| if (conversationId == -1) { |
| LogUtil.e("deleteStoredConversation: invalid thread id"); |
| return false; |
| } |
| final Uri uri = ContentUris.withAppendedId( |
| Telephony.Threads.CONTENT_URI, conversationId); |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| if (getContentResolver().delete(uri, null, null) != 1) { |
| LogUtil.e("deleteStoredConversation: failed to delete"); |
| return false; |
| } |
| } catch (SQLiteException e) { |
| LogUtil.e("deleteStoredConversation: failed to delete", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean updateStoredMessageStatus(String callingPkg, Uri messageUri, |
| ContentValues statusValues) throws RemoteException { |
| LogUtil.d("updateStoredMessageStatus " + messageUri); |
| enforceSystemUid(); |
| return updateMessageStatus(messageUri, statusValues); |
| } |
| |
| @Override |
| public boolean archiveStoredConversation(String callingPkg, long conversationId, |
| boolean archived) throws RemoteException { |
| LogUtil.d("archiveStoredConversation " + conversationId + " " + archived); |
| if (conversationId == -1) { |
| LogUtil.e("archiveStoredConversation: invalid thread id"); |
| return false; |
| } |
| return archiveConversation(conversationId, archived); |
| } |
| |
| @Override |
| public Uri addTextMessageDraft(String callingPkg, String address, String text) |
| throws RemoteException { |
| LogUtil.d("addTextMessageDraft"); |
| enforceSystemUid(); |
| return addSmsDraft(address, text, callingPkg); |
| } |
| |
| @Override |
| public Uri addMultimediaMessageDraft(String callingPkg, Uri contentUri) |
| throws RemoteException { |
| LogUtil.d("addMultimediaMessageDraft"); |
| enforceSystemUid(); |
| return addMmsDraft(contentUri, callingPkg); |
| } |
| |
| @Override |
| public void sendStoredMessage(int subId, String callingPkg, Uri messageUri, |
| Bundle configOverrides, PendingIntent sentIntent) throws RemoteException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void setAutoPersisting(String callingPkg, boolean enabled) throws RemoteException { |
| LogUtil.d("setAutoPersisting " + enabled); |
| enforceSystemUid(); |
| final SharedPreferences preferences = getSharedPreferences( |
| SHARED_PREFERENCES_NAME, MODE_PRIVATE); |
| final SharedPreferences.Editor editor = preferences.edit(); |
| editor.putBoolean(PREF_AUTO_PERSISTING, enabled); |
| editor.apply(); |
| } |
| |
| @Override |
| public boolean getAutoPersisting() throws RemoteException { |
| LogUtil.d("getAutoPersisting"); |
| return getAutoPersistingPref(); |
| } |
| |
| /* |
| * @return true if the subId is active. |
| */ |
| private boolean isActiveSubId(int subId) { |
| return ((SubscriptionManager) getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)) |
| .isActiveSubscriptionId(subId); |
| } |
| |
| /* |
| * Calls the pending intent with <code>MMS_ERROR_NO_DATA_NETWORK</code>. |
| */ |
| private void sendErrorInPendingIntent(@Nullable PendingIntent intent) { |
| if (intent != null) { |
| try { |
| intent.send(SmsManager.MMS_ERROR_NO_DATA_NETWORK); |
| } catch (PendingIntent.CanceledException ex) { |
| } |
| } |
| } |
| |
| private boolean isRawPduSendReq(Uri contentUri) { |
| // X-Mms-Message-Type is at the beginning of the message headers always. 1st byte is |
| // MMS-filed-name and 2nd byte is MMS-value for X-Mms-Message-Type field. |
| // See OMA-TS-MMS_ENC-V1_3-20110913-A, 7. Binary Encoding of ProtocolData Units |
| byte[] pduData = new byte[2]; |
| int bytesRead = readPduBytesFromContentUri(contentUri, pduData); |
| |
| // Return true for MESSAGE_TYPE_SEND_REQ only. Otherwise false even wrong PDU case. |
| if (bytesRead == 2 |
| && (pduData[0] & 0xFF) == MESSAGE_TYPE |
| && (pduData[1] & 0xFF) == MESSAGE_TYPE_SEND_REQ) { |
| return true; |
| } |
| return false; |
| } |
| }; |
| |
| @Override |
| public void addSimRequest(MmsRequest request) { |
| if (request == null) { |
| LogUtil.e("Add running or pending: empty request"); |
| return; |
| } |
| LogUtil.d("Current running=" + mRunningRequestCount + ", " |
| + "current subId=" + mCurrentSubId + ", " |
| + "pending=" + mPendingSimRequestQueue.size()); |
| synchronized (this) { |
| if (mPendingSimRequestQueue.size() > 0 || |
| (mRunningRequestCount > 0 && request.getSubId() != mCurrentSubId)) { |
| LogUtil.d("Add request to pending queue." |
| + " Request subId=" + request.getSubId() + "," |
| + " current subId=" + mCurrentSubId); |
| mPendingSimRequestQueue.add(request); |
| if (mRunningRequestCount <= 0) { |
| LogUtil.e("Nothing's running but queue's not empty"); |
| // Nothing is running but we are accumulating on pending queue. |
| // This should not happen. But just in case... |
| movePendingSimRequestsToRunningSynchronized(); |
| } |
| } else { |
| addToRunningRequestQueueSynchronized(request); |
| } |
| } |
| } |
| |
| private void sendSettingsIntentForFailedMms(boolean isIncoming, int subId) { |
| LogUtil.w("Subscription with id: " + subId |
| + " cannot " + (isIncoming ? "download" : "send") |
| + " MMS, data connection is not available"); |
| Intent intent = new Intent(Settings.ACTION_ENABLE_MMS_DATA_REQUEST); |
| |
| intent.putExtra(Settings.EXTRA_ENABLE_MMS_DATA_REQUEST_REASON, |
| isIncoming ? Settings.ENABLE_MMS_DATA_REQUEST_REASON_INCOMING_MMS |
| : Settings.ENABLE_MMS_DATA_REQUEST_REASON_OUTGOING_MMS); |
| |
| intent.putExtra(Settings.EXTRA_SUB_ID, subId); |
| |
| this.sendBroadcastAsUser(intent, UserHandle.SYSTEM, |
| android.Manifest.permission.NETWORK_SETTINGS); |
| } |
| |
| private void addToRunningRequestQueueSynchronized(final MmsRequest request) { |
| LogUtil.d("Add request to running queue for subId " + request.getSubId()); |
| // Update current state of running requests |
| final int queue = request.getQueueType(); |
| if (queue < 0 || queue >= mRunningRequestExecutors.length) { |
| LogUtil.e("Invalid request queue index for running request"); |
| return; |
| } |
| mRunningRequestCount++; |
| mCurrentSubId = request.getSubId(); |
| // Send to the corresponding request queue for execution |
| mRunningRequestExecutors[queue].execute(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| request.execute(MmsService.this, getNetworkManager(request.getSubId())); |
| } finally { |
| synchronized (MmsService.this) { |
| mRunningRequestCount--; |
| if (mRunningRequestCount <= 0) { |
| movePendingSimRequestsToRunningSynchronized(); |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| private void movePendingSimRequestsToRunningSynchronized() { |
| LogUtil.d("Schedule requests pending on SIM"); |
| mCurrentSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| while (mPendingSimRequestQueue.size() > 0) { |
| final MmsRequest request = mPendingSimRequestQueue.peek(); |
| if (request != null) { |
| if (!SubscriptionManager.isValidSubscriptionId(mCurrentSubId) |
| || mCurrentSubId == request.getSubId()) { |
| // First or subsequent requests with same SIM ID |
| mPendingSimRequestQueue.remove(); |
| addToRunningRequestQueueSynchronized(request); |
| } else { |
| // Stop if we see a different SIM ID |
| break; |
| } |
| } else { |
| LogUtil.e("Schedule pending: found empty request"); |
| mPendingSimRequestQueue.remove(); |
| } |
| } |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mStub; |
| } |
| |
| public final IBinder asBinder() { |
| return mStub; |
| } |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| LogUtil.d("onCreate"); |
| // Load mms_config |
| MmsConfigManager.getInstance().init(this); |
| // Initialize running request state |
| for (int i = 0; i < mRunningRequestExecutors.length; i++) { |
| mRunningRequestExecutors[i] = Executors.newFixedThreadPool(THREAD_POOL_SIZE); |
| } |
| synchronized (this) { |
| mCurrentSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| mRunningRequestCount = 0; |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| LogUtil.d("onDestroy"); |
| for (ExecutorService executor : mRunningRequestExecutors) { |
| executor.shutdown(); |
| } |
| } |
| |
| private Uri importSms(String address, int type, String text, long timestampMillis, |
| boolean seen, boolean read, String creator) { |
| Uri insertUri = null; |
| switch (type) { |
| case SMS_TYPE_INCOMING: |
| insertUri = Telephony.Sms.Inbox.CONTENT_URI; |
| |
| break; |
| case SMS_TYPE_OUTGOING: |
| insertUri = Telephony.Sms.Sent.CONTENT_URI; |
| break; |
| } |
| if (insertUri == null) { |
| LogUtil.e("importTextMessage: invalid message type for importing: " + type); |
| return null; |
| } |
| final ContentValues values = new ContentValues(6); |
| values.put(Telephony.Sms.ADDRESS, address); |
| values.put(Telephony.Sms.DATE, timestampMillis); |
| values.put(Telephony.Sms.SEEN, seen ? 1 : 0); |
| values.put(Telephony.Sms.READ, read ? 1 : 0); |
| values.put(Telephony.Sms.BODY, text); |
| if (!TextUtils.isEmpty(creator)) { |
| values.put(Telephony.Mms.CREATOR, creator); |
| } |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| return getContentResolver().insert(insertUri, values); |
| } catch (SQLiteException e) { |
| LogUtil.e("importTextMessage: failed to persist imported text message", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return null; |
| } |
| |
| private Uri importMms(Uri contentUri, String messageId, long timestampSecs, |
| boolean seen, boolean read, String creator) { |
| byte[] pduData = readPduFromContentUri(contentUri, MAX_MMS_FILE_SIZE); |
| if (pduData == null || pduData.length < 1) { |
| LogUtil.e("importMessage: empty PDU"); |
| return null; |
| } |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| final GenericPdu pdu = parsePduForAnyCarrier(pduData); |
| if (pdu == null) { |
| LogUtil.e("importMessage: can't parse input PDU"); |
| return null; |
| } |
| Uri insertUri = null; |
| if (pdu instanceof SendReq) { |
| insertUri = Telephony.Mms.Sent.CONTENT_URI; |
| } else if (pdu instanceof RetrieveConf || |
| pdu instanceof NotificationInd || |
| pdu instanceof DeliveryInd || |
| pdu instanceof ReadOrigInd) { |
| insertUri = Telephony.Mms.Inbox.CONTENT_URI; |
| } |
| if (insertUri == null) { |
| LogUtil.e("importMessage; invalid MMS type: " + pdu.getClass().getCanonicalName()); |
| return null; |
| } |
| final PduPersister persister = PduPersister.getPduPersister(this); |
| final Uri uri = persister.persist( |
| pdu, |
| insertUri, |
| true/*createThreadId*/, |
| true/*groupMmsEnabled*/, |
| null/*preOpenedFiles*/); |
| if (uri == null) { |
| LogUtil.e("importMessage: failed to persist message"); |
| return null; |
| } |
| final ContentValues values = new ContentValues(5); |
| if (!TextUtils.isEmpty(messageId)) { |
| values.put(Telephony.Mms.MESSAGE_ID, messageId); |
| } |
| if (timestampSecs != -1) { |
| values.put(Telephony.Mms.DATE, timestampSecs); |
| } |
| values.put(Telephony.Mms.READ, seen ? 1 : 0); |
| values.put(Telephony.Mms.SEEN, read ? 1 : 0); |
| if (!TextUtils.isEmpty(creator)) { |
| values.put(Telephony.Mms.CREATOR, creator); |
| } |
| if (SqliteWrapper.update(this, getContentResolver(), uri, values, |
| null/*where*/, null/*selectionArg*/) != 1) { |
| LogUtil.e("importMessage: failed to update message"); |
| } |
| return uri; |
| } catch (RuntimeException e) { |
| LogUtil.e("importMessage: failed to parse input PDU", e); |
| } catch (MmsException e) { |
| LogUtil.e("importMessage: failed to persist message", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return null; |
| } |
| |
| private static boolean isSmsMmsContentUri(Uri uri) { |
| final String uriString = uri.toString(); |
| if (!uriString.startsWith("content://sms/") && !uriString.startsWith("content://mms/")) { |
| return false; |
| } |
| if (ContentUris.parseId(uri) == -1) { |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean updateMessageStatus(Uri messageUri, ContentValues statusValues) { |
| if (!isSmsMmsContentUri(messageUri)) { |
| LogUtil.e("updateMessageStatus: invalid messageUri: " + messageUri.toString()); |
| return false; |
| } |
| if (statusValues == null) { |
| LogUtil.w("updateMessageStatus: empty values to update"); |
| return false; |
| } |
| final ContentValues values = new ContentValues(); |
| if (statusValues.containsKey(MESSAGE_STATUS_READ)) { |
| final Integer val = statusValues.getAsInteger(MESSAGE_STATUS_READ); |
| if (val != null) { |
| // MMS uses the same column name |
| values.put(Telephony.Sms.READ, val); |
| } |
| } else if (statusValues.containsKey(MESSAGE_STATUS_SEEN)) { |
| final Integer val = statusValues.getAsInteger(MESSAGE_STATUS_SEEN); |
| if (val != null) { |
| // MMS uses the same column name |
| values.put(Telephony.Sms.SEEN, val); |
| } |
| } |
| if (values.size() < 1) { |
| LogUtil.w("updateMessageStatus: no value to update"); |
| return false; |
| } |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| if (getContentResolver().update( |
| messageUri, values, null/*where*/, null/*selectionArgs*/) != 1) { |
| LogUtil.e("updateMessageStatus: failed to update database"); |
| return false; |
| } |
| return true; |
| } catch (SQLiteException e) { |
| LogUtil.e("updateMessageStatus: failed to update database", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return false; |
| } |
| |
| private static final String ARCHIVE_CONVERSATION_SELECTION = Telephony.Threads._ID + "=?"; |
| |
| private boolean archiveConversation(long conversationId, boolean archived) { |
| final ContentValues values = new ContentValues(1); |
| values.put(Telephony.Threads.ARCHIVED, archived ? 1 : 0); |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| if (getContentResolver().update( |
| Telephony.Threads.CONTENT_URI, |
| values, |
| ARCHIVE_CONVERSATION_SELECTION, |
| new String[]{Long.toString(conversationId)}) != 1) { |
| LogUtil.e("archiveConversation: failed to update database"); |
| return false; |
| } |
| return true; |
| } catch (SQLiteException e) { |
| LogUtil.e("archiveConversation: failed to update database", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return false; |
| } |
| |
| private Uri addSmsDraft(String address, String text, String creator) { |
| final ContentValues values = new ContentValues(5); |
| values.put(Telephony.Sms.ADDRESS, address); |
| values.put(Telephony.Sms.BODY, text); |
| values.put(Telephony.Sms.READ, 1); |
| values.put(Telephony.Sms.SEEN, 1); |
| if (!TextUtils.isEmpty(creator)) { |
| values.put(Telephony.Mms.CREATOR, creator); |
| } |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| return getContentResolver().insert(Telephony.Sms.Draft.CONTENT_URI, values); |
| } catch (SQLiteException e) { |
| LogUtil.e("addSmsDraft: failed to store draft message", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return null; |
| } |
| |
| private Uri addMmsDraft(Uri contentUri, String creator) { |
| byte[] pduData = readPduFromContentUri(contentUri, MAX_MMS_FILE_SIZE); |
| if (pduData == null || pduData.length < 1) { |
| LogUtil.e("addMmsDraft: empty PDU"); |
| return null; |
| } |
| // Clear the calling identity and query the database using the phone user id |
| // Otherwise the AppOps check in TelephonyProvider would complain about mismatch |
| // between the calling uid and the package uid |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| final GenericPdu pdu = parsePduForAnyCarrier(pduData); |
| if (pdu == null) { |
| LogUtil.e("addMmsDraft: can't parse input PDU"); |
| return null; |
| } |
| if (!(pdu instanceof SendReq)) { |
| LogUtil.e("addMmsDraft; invalid MMS type: " + pdu.getClass().getCanonicalName()); |
| return null; |
| } |
| final PduPersister persister = PduPersister.getPduPersister(this); |
| final Uri uri = persister.persist( |
| pdu, |
| Telephony.Mms.Draft.CONTENT_URI, |
| true/*createThreadId*/, |
| true/*groupMmsEnabled*/, |
| null/*preOpenedFiles*/); |
| if (uri == null) { |
| LogUtil.e("addMmsDraft: failed to persist message"); |
| return null; |
| } |
| final ContentValues values = new ContentValues(3); |
| values.put(Telephony.Mms.READ, 1); |
| values.put(Telephony.Mms.SEEN, 1); |
| if (!TextUtils.isEmpty(creator)) { |
| values.put(Telephony.Mms.CREATOR, creator); |
| } |
| if (SqliteWrapper.update(this, getContentResolver(), uri, values, |
| null/*where*/, null/*selectionArg*/) != 1) { |
| LogUtil.e("addMmsDraft: failed to update message"); |
| } |
| return uri; |
| } catch (RuntimeException e) { |
| LogUtil.e("addMmsDraft: failed to parse input PDU", e); |
| } catch (MmsException e) { |
| LogUtil.e("addMmsDraft: failed to persist message", e); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| return null; |
| } |
| |
| /** |
| * Try parsing a PDU without knowing the carrier. This is useful for importing |
| * MMS or storing draft when carrier info is not available |
| * |
| * @param data The PDU data |
| * @return Parsed PDU, null if failed to parse |
| */ |
| private static GenericPdu parsePduForAnyCarrier(final byte[] data) { |
| GenericPdu pdu = null; |
| try { |
| pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse(); |
| } catch (RuntimeException e) { |
| LogUtil.w("parsePduForAnyCarrier: Failed to parse PDU with content disposition", e); |
| } |
| if (pdu == null) { |
| try { |
| pdu = (new PduParser(data, false/*parseContentDisposition*/)).parse(); |
| } catch (RuntimeException e) { |
| LogUtil.w("parsePduForAnyCarrier: Failed to parse PDU without content disposition", |
| e); |
| } |
| } |
| return pdu; |
| } |
| |
| @Override |
| public boolean getAutoPersistingPref() { |
| final SharedPreferences preferences = getSharedPreferences( |
| SHARED_PREFERENCES_NAME, MODE_PRIVATE); |
| return preferences.getBoolean(PREF_AUTO_PERSISTING, false); |
| } |
| |
| /** |
| * Read pdu from content provider uri. |
| * |
| * @param contentUri content provider uri from which to read. |
| * @param maxSize maximum number of bytes to read. |
| * @return pdu bytes if succeeded else null. |
| */ |
| public byte[] readPduFromContentUri(final Uri contentUri, final int maxSize) { |
| // Request one extra byte to make sure file not bigger than maxSize |
| byte[] pduData = new byte[maxSize + 1]; |
| int bytesRead = readPduBytesFromContentUri(contentUri, pduData); |
| if (bytesRead <= 0) { |
| return null; |
| } |
| if (bytesRead > maxSize) { |
| LogUtil.e("PDU read is too large"); |
| return null; |
| } |
| return Arrays.copyOf(pduData, bytesRead); |
| } |
| |
| /** |
| * Read up to length of the pduData array from content provider uri. |
| * |
| * @param contentUri content provider uri from which to read. |
| * @param pduData the buffer into which the data is read. |
| * @return the total number of bytes read into the pduData. |
| */ |
| public int readPduBytesFromContentUri(final Uri contentUri, byte[] pduData) { |
| if (contentUri == null) { |
| LogUtil.e("Uri is null"); |
| return 0; |
| } |
| Callable<Integer> copyPduToArray = new Callable<Integer>() { |
| public Integer call() { |
| ParcelFileDescriptor.AutoCloseInputStream inStream = null; |
| try { |
| ContentResolver cr = MmsService.this.getContentResolver(); |
| ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "r"); |
| inStream = new ParcelFileDescriptor.AutoCloseInputStream(pduFd); |
| int bytesRead = inStream.read(pduData, 0, pduData.length); |
| if (bytesRead <= 0) { |
| LogUtil.e("Empty PDU or at end of the file"); |
| } |
| return bytesRead; |
| } catch (IOException ex) { |
| LogUtil.e("IO exception reading PDU", ex); |
| return 0; |
| } finally { |
| if (inStream != null) { |
| try { |
| inStream.close(); |
| } catch (IOException ex) { |
| } |
| } |
| } |
| } |
| }; |
| |
| final Future<Integer> pendingResult = mPduTransferExecutor.submit(copyPduToArray); |
| try { |
| return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } catch (Exception e) { |
| // Typically a timeout occurred - cancel task |
| pendingResult.cancel(true); |
| } |
| return 0; |
| } |
| |
| /** |
| * Write pdu bytes to content provider uri |
| * |
| * @param contentUri content provider uri to which bytes should be written |
| * @param pdu Bytes to write |
| * @return true if all bytes successfully written else false |
| */ |
| public boolean writePduToContentUri(final Uri contentUri, final byte[] pdu) { |
| if (contentUri == null || pdu == null) { |
| return false; |
| } |
| final Callable<Boolean> copyDownloadedPduToOutput = new Callable<Boolean>() { |
| public Boolean call() { |
| ParcelFileDescriptor.AutoCloseOutputStream outStream = null; |
| try { |
| ContentResolver cr = MmsService.this.getContentResolver(); |
| ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "w"); |
| outStream = new ParcelFileDescriptor.AutoCloseOutputStream(pduFd); |
| outStream.write(pdu); |
| return Boolean.TRUE; |
| } catch (IOException ex) { |
| LogUtil.e("IO exception writing PDU", ex); |
| return Boolean.FALSE; |
| } finally { |
| if (outStream != null) { |
| try { |
| outStream.close(); |
| } catch (IOException ex) { |
| } |
| } |
| } |
| } |
| }; |
| |
| final Future<Boolean> pendingResult = |
| mPduTransferExecutor.submit(copyDownloadedPduToOutput); |
| try { |
| return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } catch (Exception e) { |
| // Typically a timeout occurred - cancel task |
| pendingResult.cancel(true); |
| } |
| return false; |
| } |
| } |