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.IBinder; 33 import android.os.ParcelFileDescriptor; 34 import android.os.Process; 35 import android.os.RemoteException; 36 import android.provider.Telephony; 37 import android.service.carrier.CarrierMessagingService; 38 import android.telephony.SmsManager; 39 import android.telephony.SubscriptionManager; 40 import android.telephony.TelephonyManager; 41 import android.text.TextUtils; 42 import android.util.SparseArray; 43 44 import com.android.internal.telephony.IMms; 45 import com.google.android.mms.MmsException; 46 import com.google.android.mms.pdu.DeliveryInd; 47 import com.google.android.mms.pdu.GenericPdu; 48 import com.google.android.mms.pdu.NotificationInd; 49 import com.google.android.mms.pdu.PduParser; 50 import com.google.android.mms.pdu.PduPersister; 51 import com.google.android.mms.pdu.ReadOrigInd; 52 import com.google.android.mms.pdu.RetrieveConf; 53 import com.google.android.mms.pdu.SendReq; 54 import com.google.android.mms.util.SqliteWrapper; 55 56 import java.io.IOException; 57 import java.util.ArrayDeque; 58 import java.util.Arrays; 59 import java.util.List; 60 import java.util.Queue; 61 import java.util.concurrent.Callable; 62 import java.util.concurrent.ExecutorService; 63 import java.util.concurrent.Executors; 64 import java.util.concurrent.Future; 65 import java.util.concurrent.TimeUnit; 66 67 /** 68 * System service to process MMS API requests 69 */ 70 public 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 getNetworkManager(int subId)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 enforceSystemUid()124 private void enforceSystemUid() { 125 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 126 throw new SecurityException("Only system can call this service"); 127 } 128 } 129 checkSubId(int subId)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 getCarrierMessagingServicePackageIfExists()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 addSimRequest(MmsRequest request)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 addToRunningRequestQueueSynchronized(final MmsRequest request)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 movePendingSimRequestsToRunningSynchronized()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 onBind(Intent intent)450 public IBinder onBind(Intent intent) { 451 return mStub; 452 } 453 asBinder()454 public final IBinder asBinder() { 455 return mStub; 456 } 457 458 @Override onCreate()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 onDestroy()475 public void onDestroy() { 476 super.onDestroy(); 477 LogUtil.d("onDestroy"); 478 for (ExecutorService executor : mRunningRequestExecutors) { 479 executor.shutdown(); 480 } 481 } 482 importSms(String address, int type, String text, long timestampMillis, boolean seen, boolean read, String creator)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 importMms(Uri contentUri, String messageId, long timestampSecs, boolean seen, boolean read, String creator)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 isSmsMmsContentUri(Uri uri)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 updateMessageStatus(Uri messageUri, ContentValues statusValues)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 + "=?"; archiveConversation(long conversationId, boolean archived)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 addSmsDraft(String address, String text, String creator)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 addMmsDraft(Uri contentUri, String creator)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 */ parsePduForAnyCarrier(final byte[] data)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 getAutoPersistingPref()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 */ readPduFromContentUri(final Uri contentUri, final int maxSize)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 */ writePduToContentUri(final Uri contentUri, final byte[] pdu)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