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