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 addPending(int key, MmsRequest request)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 { RequestQueue(Looper looper)105 public RequestQueue(Looper looper) { 106 super(looper); 107 } 108 109 @Override handleMessage(Message msg)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 enforceSystemUid()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 */ startRequestQueueIfNeeded(int queueIndex)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 addRunning(MmsRequest request)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 onBind(Intent intent)373 public IBinder onBind(Intent intent) { 374 return mStub; 375 } 376 asBinder()377 public final IBinder asBinder() { 378 return mStub; 379 } 380 381 @Override onCreate()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 importSms(String address, int type, String text, long timestampMillis, boolean seen, boolean read, String creator)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 importMms(Uri contentUri, String messageId, long timestampSecs, boolean seen, boolean read, String creator)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 isSmsMmsContentUri(Uri uri)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 updateMessageStatus(Uri messageUri, ContentValues statusValues)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 + "=?"; archiveConversation(long conversationId, boolean archived)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 addSmsDraft(String address, String text, String creator)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 addMmsDraft(Uri contentUri, String creator)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 isFailedOrDraft(Uri messageUri)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 loadPdu(Uri messageUri)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 returnUnspecifiedFailure(PendingIntent pi)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 getAutoPersistingPref()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 */ readPduFromContentUri(final Uri contentUri, final int maxSize)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 */ writePduToContentUri(final Uri contentUri, final byte[] pdu)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