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