1 /* 2 * Copyright (C) 2008 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.internal.telephony; 18 19 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND; 20 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND; 21 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND; 22 import android.app.Activity; 23 import android.app.AppOpsManager; 24 import android.app.BroadcastOptions; 25 import android.content.BroadcastReceiver; 26 import android.content.ComponentName; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.ServiceConnection; 31 import android.database.Cursor; 32 import android.database.DatabaseUtils; 33 import android.database.sqlite.SQLiteException; 34 import android.database.sqlite.SqliteWrapper; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.IBinder; 38 import android.os.IDeviceIdleController; 39 import android.os.RemoteException; 40 import android.os.ServiceManager; 41 import android.os.UserHandle; 42 import android.provider.Telephony; 43 import android.provider.Telephony.Sms.Intents; 44 import android.telephony.Rlog; 45 import android.telephony.SmsManager; 46 import android.telephony.SubscriptionManager; 47 import android.util.Log; 48 49 import com.android.internal.telephony.uicc.IccUtils; 50 51 import java.util.HashMap; 52 53 import com.google.android.mms.MmsException; 54 import com.google.android.mms.pdu.DeliveryInd; 55 import com.google.android.mms.pdu.GenericPdu; 56 import com.google.android.mms.pdu.NotificationInd; 57 import com.google.android.mms.pdu.PduHeaders; 58 import com.google.android.mms.pdu.PduParser; 59 import com.google.android.mms.pdu.PduPersister; 60 import com.google.android.mms.pdu.ReadOrigInd; 61 62 /** 63 * WAP push handler class. 64 * 65 * @hide 66 */ 67 public class WapPushOverSms implements ServiceConnection { 68 private static final String TAG = "WAP PUSH"; 69 private static final boolean DBG = false; 70 71 private final Context mContext; 72 private IDeviceIdleController mDeviceIdleController; 73 74 private String mWapPushManagerPackage; 75 76 /** Assigned from ServiceConnection callback on main threaad. */ 77 private volatile IWapPushManager mWapPushManager; 78 79 @Override 80 public void onServiceConnected(ComponentName name, IBinder service) { 81 mWapPushManager = IWapPushManager.Stub.asInterface(service); 82 if (DBG) Rlog.v(TAG, "wappush manager connected to " + hashCode()); 83 } 84 85 @Override 86 public void onServiceDisconnected(ComponentName name) { 87 mWapPushManager = null; 88 if (DBG) Rlog.v(TAG, "wappush manager disconnected."); 89 } 90 91 public WapPushOverSms(Context context) { 92 mContext = context; 93 mDeviceIdleController = TelephonyComponentFactory.getInstance().getIDeviceIdleController(); 94 Intent intent = new Intent(IWapPushManager.class.getName()); 95 ComponentName comp = intent.resolveSystemService(context.getPackageManager(), 0); 96 intent.setComponent(comp); 97 if (comp == null || !context.bindService(intent, this, Context.BIND_AUTO_CREATE)) { 98 Rlog.e(TAG, "bindService() for wappush manager failed"); 99 } else { 100 mWapPushManagerPackage = comp.getPackageName(); 101 if (DBG) Rlog.v(TAG, "bindService() for wappush manager succeeded"); 102 } 103 } 104 105 public void dispose() { 106 if (mWapPushManager != null) { 107 if (DBG) Rlog.v(TAG, "dispose: unbind wappush manager"); 108 mContext.unbindService(this); 109 } else { 110 Rlog.e(TAG, "dispose: not bound to a wappush manager"); 111 } 112 } 113 114 /** 115 * Decodes the wap push pdu. The decoded result is wrapped inside the {@link DecodedResult} 116 * object. The caller of this method should check {@link DecodedResult#statusCode} for the 117 * decoding status. It can have the following values. 118 * 119 * Activity.RESULT_OK - the wap push pdu is successfully decoded and should be further processed 120 * Intents.RESULT_SMS_HANDLED - the wap push pdu should be ignored. 121 * Intents.RESULT_SMS_GENERIC_ERROR - the pdu is invalid. 122 */ 123 private DecodedResult decodeWapPdu(byte[] pdu, InboundSmsHandler handler) { 124 DecodedResult result = new DecodedResult(); 125 if (DBG) Rlog.d(TAG, "Rx: " + IccUtils.bytesToHexString(pdu)); 126 127 try { 128 int index = 0; 129 int transactionId = pdu[index++] & 0xFF; 130 int pduType = pdu[index++] & 0xFF; 131 132 // Should we "abort" if no subId for now just no supplying extra param below 133 int phoneId = handler.getPhone().getPhoneId(); 134 135 if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) && 136 (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) { 137 index = mContext.getResources().getInteger( 138 com.android.internal.R.integer.config_valid_wappush_index); 139 if (index != -1) { 140 transactionId = pdu[index++] & 0xff; 141 pduType = pdu[index++] & 0xff; 142 if (DBG) 143 Rlog.d(TAG, "index = " + index + " PDU Type = " + pduType + 144 " transactionID = " + transactionId); 145 146 // recheck wap push pduType 147 if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) 148 && (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) { 149 if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType); 150 result.statusCode = Intents.RESULT_SMS_HANDLED; 151 return result; 152 } 153 } else { 154 if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType); 155 result.statusCode = Intents.RESULT_SMS_HANDLED; 156 return result; 157 } 158 } 159 160 WspTypeDecoder pduDecoder = 161 TelephonyComponentFactory.getInstance().makeWspTypeDecoder(pdu); 162 163 /** 164 * Parse HeaderLen(unsigned integer). 165 * From wap-230-wsp-20010705-a section 8.1.2 166 * The maximum size of a uintvar is 32 bits. 167 * So it will be encoded in no more than 5 octets. 168 */ 169 if (pduDecoder.decodeUintvarInteger(index) == false) { 170 if (DBG) Rlog.w(TAG, "Received PDU. Header Length error."); 171 result.statusCode = Intents.RESULT_SMS_GENERIC_ERROR; 172 return result; 173 } 174 int headerLength = (int) pduDecoder.getValue32(); 175 index += pduDecoder.getDecodedDataLength(); 176 177 int headerStartIndex = index; 178 179 /** 180 * Parse Content-Type. 181 * From wap-230-wsp-20010705-a section 8.4.2.24 182 * 183 * Content-type-value = Constrained-media | Content-general-form 184 * Content-general-form = Value-length Media-type 185 * Media-type = (Well-known-media | Extension-Media) *(Parameter) 186 * Value-length = Short-length | (Length-quote Length) 187 * Short-length = <Any octet 0-30> (octet <= WAP_PDU_SHORT_LENGTH_MAX) 188 * Length-quote = <Octet 31> (WAP_PDU_LENGTH_QUOTE) 189 * Length = Uintvar-integer 190 */ 191 if (pduDecoder.decodeContentType(index) == false) { 192 if (DBG) Rlog.w(TAG, "Received PDU. Header Content-Type error."); 193 result.statusCode = Intents.RESULT_SMS_GENERIC_ERROR; 194 return result; 195 } 196 197 String mimeType = pduDecoder.getValueString(); 198 long binaryContentType = pduDecoder.getValue32(); 199 index += pduDecoder.getDecodedDataLength(); 200 201 byte[] header = new byte[headerLength]; 202 System.arraycopy(pdu, headerStartIndex, header, 0, header.length); 203 204 byte[] intentData; 205 206 if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) { 207 intentData = pdu; 208 } else { 209 int dataIndex = headerStartIndex + headerLength; 210 intentData = new byte[pdu.length - dataIndex]; 211 System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length); 212 } 213 214 int[] subIds = SubscriptionManager.getSubId(phoneId); 215 int subId = (subIds != null) && (subIds.length > 0) ? subIds[0] 216 : SmsManager.getDefaultSmsSubscriptionId(); 217 218 // Continue if PDU parsing fails: the default messaging app may successfully parse the 219 // same PDU. 220 GenericPdu parsedPdu = null; 221 try { 222 parsedPdu = new PduParser(intentData, shouldParseContentDisposition(subId)).parse(); 223 } catch (Exception e) { 224 Rlog.e(TAG, "Unable to parse PDU: " + e.toString()); 225 } 226 227 if (parsedPdu != null && parsedPdu.getMessageType() == MESSAGE_TYPE_NOTIFICATION_IND) { 228 final NotificationInd nInd = (NotificationInd) parsedPdu; 229 if (nInd.getFrom() != null 230 && BlockChecker.isBlocked(mContext, nInd.getFrom().getString())) { 231 result.statusCode = Intents.RESULT_SMS_HANDLED; 232 return result; 233 } 234 } 235 236 /** 237 * Seek for application ID field in WSP header. 238 * If application ID is found, WapPushManager substitute the message 239 * processing. Since WapPushManager is optional module, if WapPushManager 240 * is not found, legacy message processing will be continued. 241 */ 242 if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) { 243 index = (int) pduDecoder.getValue32(); 244 pduDecoder.decodeXWapApplicationId(index); 245 String wapAppId = pduDecoder.getValueString(); 246 if (wapAppId == null) { 247 wapAppId = Integer.toString((int) pduDecoder.getValue32()); 248 } 249 result.wapAppId = wapAppId; 250 String contentType = ((mimeType == null) ? 251 Long.toString(binaryContentType) : mimeType); 252 result.contentType = contentType; 253 if (DBG) Rlog.v(TAG, "appid found: " + wapAppId + ":" + contentType); 254 } 255 256 result.subId = subId; 257 result.phoneId = phoneId; 258 result.parsedPdu = parsedPdu; 259 result.mimeType = mimeType; 260 result.transactionId = transactionId; 261 result.pduType = pduType; 262 result.header = header; 263 result.intentData = intentData; 264 result.contentTypeParameters = pduDecoder.getContentParameters(); 265 result.statusCode = Activity.RESULT_OK; 266 } catch (ArrayIndexOutOfBoundsException aie) { 267 // 0-byte WAP PDU or other unexpected WAP PDU contents can easily throw this; 268 // log exception string without stack trace and return false. 269 Rlog.e(TAG, "ignoring dispatchWapPdu() array index exception: " + aie); 270 result.statusCode = Intents.RESULT_SMS_GENERIC_ERROR; 271 } 272 return result; 273 } 274 275 /** 276 * Dispatches inbound messages that are in the WAP PDU format. See 277 * wap-230-wsp-20010705-a section 8 for details on the WAP PDU format. 278 * 279 * @param pdu The WAP PDU, made up of one or more SMS PDUs 280 * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or 281 * {@link Activity#RESULT_OK} if the message has been broadcast 282 * to applications 283 */ 284 public int dispatchWapPdu(byte[] pdu, BroadcastReceiver receiver, InboundSmsHandler handler) { 285 DecodedResult result = decodeWapPdu(pdu, handler); 286 if (result.statusCode != Activity.RESULT_OK) { 287 return result.statusCode; 288 } 289 290 if (SmsManager.getDefault().getAutoPersisting()) { 291 // Store the wap push data in telephony 292 writeInboxMessage(result.subId, result.parsedPdu); 293 } 294 295 /** 296 * If the pdu has application ID, WapPushManager substitute the message 297 * processing. Since WapPushManager is optional module, if WapPushManager 298 * is not found, legacy message processing will be continued. 299 */ 300 if (result.wapAppId != null) { 301 try { 302 boolean processFurther = true; 303 IWapPushManager wapPushMan = mWapPushManager; 304 305 if (wapPushMan == null) { 306 if (DBG) Rlog.w(TAG, "wap push manager not found!"); 307 } else { 308 mDeviceIdleController.addPowerSaveTempWhitelistAppForMms( 309 mWapPushManagerPackage, 0, "mms-mgr"); 310 311 Intent intent = new Intent(); 312 intent.putExtra("transactionId", result.transactionId); 313 intent.putExtra("pduType", result.pduType); 314 intent.putExtra("header", result.header); 315 intent.putExtra("data", result.intentData); 316 intent.putExtra("contentTypeParameters", result.contentTypeParameters); 317 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, result.phoneId); 318 319 int procRet = wapPushMan.processMessage( 320 result.wapAppId, result.contentType, intent); 321 if (DBG) Rlog.v(TAG, "procRet:" + procRet); 322 if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0 323 && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) { 324 processFurther = false; 325 } 326 } 327 if (!processFurther) { 328 return Intents.RESULT_SMS_HANDLED; 329 } 330 } catch (RemoteException e) { 331 if (DBG) Rlog.w(TAG, "remote func failed..."); 332 } 333 } 334 if (DBG) Rlog.v(TAG, "fall back to existing handler"); 335 336 if (result.mimeType == null) { 337 if (DBG) Rlog.w(TAG, "Header Content-Type error."); 338 return Intents.RESULT_SMS_GENERIC_ERROR; 339 } 340 341 Intent intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION); 342 intent.setType(result.mimeType); 343 intent.putExtra("transactionId", result.transactionId); 344 intent.putExtra("pduType", result.pduType); 345 intent.putExtra("header", result.header); 346 intent.putExtra("data", result.intentData); 347 intent.putExtra("contentTypeParameters", result.contentTypeParameters); 348 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, result.phoneId); 349 350 // Direct the intent to only the default MMS app. If we can't find a default MMS app 351 // then sent it to all broadcast receivers. 352 ComponentName componentName = SmsApplication.getDefaultMmsApplication(mContext, true); 353 Bundle options = null; 354 if (componentName != null) { 355 // Deliver MMS message only to this receiver 356 intent.setComponent(componentName); 357 if (DBG) Rlog.v(TAG, "Delivering MMS to: " + componentName.getPackageName() + 358 " " + componentName.getClassName()); 359 try { 360 long duration = mDeviceIdleController.addPowerSaveTempWhitelistAppForMms( 361 componentName.getPackageName(), 0, "mms-app"); 362 BroadcastOptions bopts = BroadcastOptions.makeBasic(); 363 bopts.setTemporaryAppWhitelistDuration(duration); 364 options = bopts.toBundle(); 365 } catch (RemoteException e) { 366 } 367 } 368 369 handler.dispatchIntent(intent, getPermissionForType(result.mimeType), 370 getAppOpsPermissionForIntent(result.mimeType), options, receiver, 371 UserHandle.SYSTEM); 372 return Activity.RESULT_OK; 373 } 374 375 /** 376 * Check whether the pdu is a MMS WAP push pdu that should be dispatched to the SMS app. 377 */ 378 public boolean isWapPushForMms(byte[] pdu, InboundSmsHandler handler) { 379 DecodedResult result = decodeWapPdu(pdu, handler); 380 return result.statusCode == Activity.RESULT_OK 381 && WspTypeDecoder.CONTENT_TYPE_B_MMS.equals(result.mimeType); 382 } 383 384 private static boolean shouldParseContentDisposition(int subId) { 385 return SmsManager 386 .getSmsManagerForSubscriptionId(subId) 387 .getCarrierConfigValues() 388 .getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION, true); 389 } 390 391 private void writeInboxMessage(int subId, GenericPdu pdu) { 392 if (pdu == null) { 393 Rlog.e(TAG, "Invalid PUSH PDU"); 394 } 395 final PduPersister persister = PduPersister.getPduPersister(mContext); 396 final int type = pdu.getMessageType(); 397 try { 398 switch (type) { 399 case MESSAGE_TYPE_DELIVERY_IND: 400 case MESSAGE_TYPE_READ_ORIG_IND: { 401 final long threadId = getDeliveryOrReadReportThreadId(mContext, pdu); 402 if (threadId == -1) { 403 // The associated SendReq isn't found, therefore skip 404 // processing this PDU. 405 Rlog.e(TAG, "Failed to find delivery or read report's thread id"); 406 break; 407 } 408 final Uri uri = persister.persist( 409 pdu, 410 Telephony.Mms.Inbox.CONTENT_URI, 411 true/*createThreadId*/, 412 true/*groupMmsEnabled*/, 413 null/*preOpenedFiles*/); 414 if (uri == null) { 415 Rlog.e(TAG, "Failed to persist delivery or read report"); 416 break; 417 } 418 // Update thread ID for ReadOrigInd & DeliveryInd. 419 final ContentValues values = new ContentValues(1); 420 values.put(Telephony.Mms.THREAD_ID, threadId); 421 if (SqliteWrapper.update( 422 mContext, 423 mContext.getContentResolver(), 424 uri, 425 values, 426 null/*where*/, 427 null/*selectionArgs*/) != 1) { 428 Rlog.e(TAG, "Failed to update delivery or read report thread id"); 429 } 430 break; 431 } 432 case MESSAGE_TYPE_NOTIFICATION_IND: { 433 final NotificationInd nInd = (NotificationInd) pdu; 434 435 Bundle configs = SmsManager.getSmsManagerForSubscriptionId(subId) 436 .getCarrierConfigValues(); 437 if (configs != null && configs.getBoolean( 438 SmsManager.MMS_CONFIG_APPEND_TRANSACTION_ID, false)) { 439 final byte [] contentLocation = nInd.getContentLocation(); 440 if ('=' == contentLocation[contentLocation.length - 1]) { 441 byte [] transactionId = nInd.getTransactionId(); 442 byte [] contentLocationWithId = new byte [contentLocation.length 443 + transactionId.length]; 444 System.arraycopy(contentLocation, 0, contentLocationWithId, 445 0, contentLocation.length); 446 System.arraycopy(transactionId, 0, contentLocationWithId, 447 contentLocation.length, transactionId.length); 448 nInd.setContentLocation(contentLocationWithId); 449 } 450 } 451 if (!isDuplicateNotification(mContext, nInd)) { 452 final Uri uri = persister.persist( 453 pdu, 454 Telephony.Mms.Inbox.CONTENT_URI, 455 true/*createThreadId*/, 456 true/*groupMmsEnabled*/, 457 null/*preOpenedFiles*/); 458 if (uri == null) { 459 Rlog.e(TAG, "Failed to save MMS WAP push notification ind"); 460 } 461 } else { 462 Rlog.d(TAG, "Skip storing duplicate MMS WAP push notification ind: " 463 + new String(nInd.getContentLocation())); 464 } 465 break; 466 } 467 default: 468 Log.e(TAG, "Received unrecognized WAP Push PDU."); 469 } 470 } catch (MmsException e) { 471 Log.e(TAG, "Failed to save MMS WAP push data: type=" + type, e); 472 } catch (RuntimeException e) { 473 Log.e(TAG, "Unexpected RuntimeException in persisting MMS WAP push data", e); 474 } 475 476 } 477 478 private static final String THREAD_ID_SELECTION = 479 Telephony.Mms.MESSAGE_ID + "=? AND " + Telephony.Mms.MESSAGE_TYPE + "=?"; 480 481 private static long getDeliveryOrReadReportThreadId(Context context, GenericPdu pdu) { 482 String messageId; 483 if (pdu instanceof DeliveryInd) { 484 messageId = new String(((DeliveryInd) pdu).getMessageId()); 485 } else if (pdu instanceof ReadOrigInd) { 486 messageId = new String(((ReadOrigInd) pdu).getMessageId()); 487 } else { 488 Rlog.e(TAG, "WAP Push data is neither delivery or read report type: " 489 + pdu.getClass().getCanonicalName()); 490 return -1L; 491 } 492 Cursor cursor = null; 493 try { 494 cursor = SqliteWrapper.query( 495 context, 496 context.getContentResolver(), 497 Telephony.Mms.CONTENT_URI, 498 new String[]{ Telephony.Mms.THREAD_ID }, 499 THREAD_ID_SELECTION, 500 new String[]{ 501 DatabaseUtils.sqlEscapeString(messageId), 502 Integer.toString(PduHeaders.MESSAGE_TYPE_SEND_REQ) 503 }, 504 null/*sortOrder*/); 505 if (cursor != null && cursor.moveToFirst()) { 506 return cursor.getLong(0); 507 } 508 } catch (SQLiteException e) { 509 Rlog.e(TAG, "Failed to query delivery or read report thread id", e); 510 } finally { 511 if (cursor != null) { 512 cursor.close(); 513 } 514 } 515 return -1L; 516 } 517 518 private static final String LOCATION_SELECTION = 519 Telephony.Mms.MESSAGE_TYPE + "=? AND " + Telephony.Mms.CONTENT_LOCATION + " =?"; 520 521 private static boolean isDuplicateNotification(Context context, NotificationInd nInd) { 522 final byte[] rawLocation = nInd.getContentLocation(); 523 if (rawLocation != null) { 524 String location = new String(rawLocation); 525 String[] selectionArgs = new String[] { location }; 526 Cursor cursor = null; 527 try { 528 cursor = SqliteWrapper.query( 529 context, 530 context.getContentResolver(), 531 Telephony.Mms.CONTENT_URI, 532 new String[]{Telephony.Mms._ID}, 533 LOCATION_SELECTION, 534 new String[]{ 535 Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND), 536 new String(rawLocation) 537 }, 538 null/*sortOrder*/); 539 if (cursor != null && cursor.getCount() > 0) { 540 // We already received the same notification before. 541 return true; 542 } 543 } catch (SQLiteException e) { 544 Rlog.e(TAG, "failed to query existing notification ind", e); 545 } finally { 546 if (cursor != null) { 547 cursor.close(); 548 } 549 } 550 } 551 return false; 552 } 553 554 public static String getPermissionForType(String mimeType) { 555 String permission; 556 if (WspTypeDecoder.CONTENT_TYPE_B_MMS.equals(mimeType)) { 557 permission = android.Manifest.permission.RECEIVE_MMS; 558 } else { 559 permission = android.Manifest.permission.RECEIVE_WAP_PUSH; 560 } 561 return permission; 562 } 563 564 public static int getAppOpsPermissionForIntent(String mimeType) { 565 int appOp; 566 if (WspTypeDecoder.CONTENT_TYPE_B_MMS.equals(mimeType)) { 567 appOp = AppOpsManager.OP_RECEIVE_MMS; 568 } else { 569 appOp = AppOpsManager.OP_RECEIVE_WAP_PUSH; 570 } 571 return appOp; 572 } 573 574 /** 575 * Place holder for decoded Wap pdu data. 576 */ 577 private final class DecodedResult { 578 String mimeType; 579 String contentType; 580 int transactionId; 581 int pduType; 582 int phoneId; 583 int subId; 584 byte[] header; 585 String wapAppId; 586 byte[] intentData; 587 HashMap<String, String> contentTypeParameters; 588 GenericPdu parsedPdu; 589 int statusCode; 590 } 591 } 592