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 { 115 public RequestQueue(Looper looper) { 116 super(looper); 117 } 118 119 @Override 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 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 150 private void enforceSystemUid() { 151 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 152 throw new SecurityException("Only system can call this service"); 153 } 154 } 155 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 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 */ 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 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 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 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 452 public IBinder onBind(Intent intent) { 453 return mStub; 454 } 455 456 public final IBinder asBinder() { 457 return mStub; 458 } 459 460 @Override 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 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 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 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 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 + "=?"; 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 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 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 */ 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 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 */ 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 */ 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