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