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