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