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