WapPushOverSms.java revision c5f9236283882b8e08390ac52e5b1fab2d3bc2fb
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.SubscriptionManager; 47import android.telephony.Rlog; 48import android.util.Log; 49 50import com.android.internal.telephony.uicc.IccUtils; 51 52import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND; 53import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND; 54import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND; 55 56/** 57 * WAP push handler class. 58 * 59 * @hide 60 */ 61public 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 // Store the wap push data in telephony 199 writeInboxMessage(intentData); 200 201 /** 202 * Seek for application ID field in WSP header. 203 * If application ID is found, WapPushManager substitute the message 204 * processing. Since WapPushManager is optional module, if WapPushManager 205 * is not found, legacy message processing will be continued. 206 */ 207 if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) { 208 index = (int) pduDecoder.getValue32(); 209 pduDecoder.decodeXWapApplicationId(index); 210 String wapAppId = pduDecoder.getValueString(); 211 if (wapAppId == null) { 212 wapAppId = Integer.toString((int) pduDecoder.getValue32()); 213 } 214 215 String contentType = ((mimeType == null) ? 216 Long.toString(binaryContentType) : mimeType); 217 if (DBG) Rlog.v(TAG, "appid found: " + wapAppId + ":" + contentType); 218 219 try { 220 boolean processFurther = true; 221 IWapPushManager wapPushMan = mWapPushManager; 222 223 if (wapPushMan == null) { 224 if (DBG) Rlog.w(TAG, "wap push manager not found!"); 225 } else { 226 Intent intent = new Intent(); 227 intent.putExtra("transactionId", transactionId); 228 intent.putExtra("pduType", pduType); 229 intent.putExtra("header", header); 230 intent.putExtra("data", intentData); 231 intent.putExtra("contentTypeParameters", 232 pduDecoder.getContentParameters()); 233 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId); 234 235 int procRet = wapPushMan.processMessage(wapAppId, contentType, intent); 236 if (DBG) Rlog.v(TAG, "procRet:" + procRet); 237 if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0 238 && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) { 239 processFurther = false; 240 } 241 } 242 if (!processFurther) { 243 return Intents.RESULT_SMS_HANDLED; 244 } 245 } catch (RemoteException e) { 246 if (DBG) Rlog.w(TAG, "remote func failed..."); 247 } 248 } 249 if (DBG) Rlog.v(TAG, "fall back to existing handler"); 250 251 if (mimeType == null) { 252 if (DBG) Rlog.w(TAG, "Header Content-Type error."); 253 return Intents.RESULT_SMS_GENERIC_ERROR; 254 } 255 256 String permission; 257 int appOp; 258 259 if (mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_MMS)) { 260 permission = android.Manifest.permission.RECEIVE_MMS; 261 appOp = AppOpsManager.OP_RECEIVE_MMS; 262 } else { 263 permission = android.Manifest.permission.RECEIVE_WAP_PUSH; 264 appOp = AppOpsManager.OP_RECEIVE_WAP_PUSH; 265 } 266 267 Intent intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION); 268 intent.setType(mimeType); 269 intent.putExtra("transactionId", transactionId); 270 intent.putExtra("pduType", pduType); 271 intent.putExtra("header", header); 272 intent.putExtra("data", intentData); 273 intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters()); 274 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId); 275 276 // Direct the intent to only the default MMS app. If we can't find a default MMS app 277 // then sent it to all broadcast receivers. 278 ComponentName componentName = SmsApplication.getDefaultMmsApplication(mContext, true); 279 if (componentName != null) { 280 // Deliver MMS message only to this receiver 281 intent.setComponent(componentName); 282 if (DBG) Rlog.v(TAG, "Delivering MMS to: " + componentName.getPackageName() + 283 " " + componentName.getClassName()); 284 } 285 286 handler.dispatchIntent(intent, permission, appOp, receiver); 287 return Activity.RESULT_OK; 288 } catch (ArrayIndexOutOfBoundsException aie) { 289 // 0-byte WAP PDU or other unexpected WAP PDU contents can easily throw this; 290 // log exception string without stack trace and return false. 291 Rlog.e(TAG, "ignoring dispatchWapPdu() array index exception: " + aie); 292 return Intents.RESULT_SMS_GENERIC_ERROR; 293 } 294 } 295 296 private void writeInboxMessage(byte[] pushData) { 297 if (!Telephony.AUTO_PERSIST) { 298 // TODO(ywen): Temporarily only enable this with a flag so not to break existing apps 299 return; 300 } 301 final GenericPdu pdu = new PduParser(pushData).parse(); 302 if (pdu == null) { 303 Rlog.e(TAG, "Invalid PUSH PDU"); 304 } 305 final PduPersister persister = PduPersister.getPduPersister(mContext); 306 final int type = pdu.getMessageType(); 307 try { 308 switch (type) { 309 case MESSAGE_TYPE_DELIVERY_IND: 310 case MESSAGE_TYPE_READ_ORIG_IND: { 311 final long threadId = getDeliveryOrReadReportThreadId(mContext, pdu); 312 if (threadId == -1) { 313 // The associated SendReq isn't found, therefore skip 314 // processing this PDU. 315 Rlog.e(TAG, "Failed to find delivery or read report's thread id"); 316 break; 317 } 318 final Uri uri = persister.persist( 319 pdu, 320 Telephony.Mms.Inbox.CONTENT_URI, 321 true/*createThreadId*/, 322 true/*groupMmsEnabled*/, 323 null/*preOpenedFiles*/); 324 if (uri == null) { 325 Rlog.e(TAG, "Failed to persist delivery or read report"); 326 break; 327 } 328 // Update thread ID for ReadOrigInd & DeliveryInd. 329 final ContentValues values = new ContentValues(1); 330 values.put(Telephony.Mms.THREAD_ID, threadId); 331 if (SqliteWrapper.update( 332 mContext, 333 mContext.getContentResolver(), 334 uri, 335 values, 336 null/*where*/, 337 null/*selectionArgs*/) != 1) { 338 Rlog.e(TAG, "Failed to update delivery or read report thread id"); 339 } 340 break; 341 } 342 case MESSAGE_TYPE_NOTIFICATION_IND: { 343 final NotificationInd nInd = (NotificationInd) pdu; 344 345 if (MessagingConfigurationManager.getDefault().getCarrierConfigBoolean( 346 MessagingConfigurationManager.CONF_APPEND_TRANSACTION_ID, false)) { 347 final byte [] contentLocation = nInd.getContentLocation(); 348 if ('=' == contentLocation[contentLocation.length - 1]) { 349 byte [] transactionId = nInd.getTransactionId(); 350 byte [] contentLocationWithId = new byte [contentLocation.length 351 + transactionId.length]; 352 System.arraycopy(contentLocation, 0, contentLocationWithId, 353 0, contentLocation.length); 354 System.arraycopy(transactionId, 0, contentLocationWithId, 355 contentLocation.length, transactionId.length); 356 nInd.setContentLocation(contentLocationWithId); 357 } 358 } 359 if (!isDuplicateNotification(mContext, nInd)) { 360 final Uri uri = persister.persist( 361 pdu, 362 Telephony.Mms.Inbox.CONTENT_URI, 363 true/*createThreadId*/, 364 true/*groupMmsEnabled*/, 365 null/*preOpenedFiles*/); 366 if (uri == null) { 367 Rlog.e(TAG, "Failed to save MMS WAP push notification ind"); 368 } 369 } else { 370 Rlog.d(TAG, "Skip storing duplicate MMS WAP push notification ind: " 371 + new String(nInd.getContentLocation())); 372 } 373 break; 374 } 375 default: 376 Log.e(TAG, "Received unrecognized WAP Push PDU."); 377 } 378 } catch (MmsException e) { 379 Log.e(TAG, "Failed to save MMS WAP push data: type=" + type, e); 380 } catch (RuntimeException e) { 381 Log.e(TAG, "Unexpected RuntimeException in persisting MMS WAP push data", e); 382 } 383 384 } 385 386 private static final String THREAD_ID_SELECTION = 387 Telephony.Mms.MESSAGE_ID + "=? AND " + Telephony.Mms.MESSAGE_TYPE + "=?"; 388 389 private static long getDeliveryOrReadReportThreadId(Context context, GenericPdu pdu) { 390 String messageId; 391 if (pdu instanceof DeliveryInd) { 392 messageId = new String(((DeliveryInd) pdu).getMessageId()); 393 } else if (pdu instanceof ReadOrigInd) { 394 messageId = new String(((ReadOrigInd) pdu).getMessageId()); 395 } else { 396 Rlog.e(TAG, "WAP Push data is neither delivery or read report type: " 397 + pdu.getClass().getCanonicalName()); 398 return -1L; 399 } 400 Cursor cursor = null; 401 try { 402 cursor = SqliteWrapper.query( 403 context, 404 context.getContentResolver(), 405 Telephony.Mms.CONTENT_URI, 406 new String[]{ Telephony.Mms.THREAD_ID }, 407 THREAD_ID_SELECTION, 408 new String[]{ 409 DatabaseUtils.sqlEscapeString(messageId), 410 Integer.toString(PduHeaders.MESSAGE_TYPE_SEND_REQ) 411 }, 412 null/*sortOrder*/); 413 if (cursor != null && cursor.moveToFirst()) { 414 return cursor.getLong(0); 415 } 416 } catch (SQLiteException e) { 417 Rlog.e(TAG, "Failed to query delivery or read report thread id", e); 418 } finally { 419 if (cursor != null) { 420 cursor.close(); 421 } 422 } 423 return -1L; 424 } 425 426 private static final String LOCATION_SELECTION = 427 Telephony.Mms.MESSAGE_TYPE + "=? AND " + Telephony.Mms.CONTENT_LOCATION + " =?"; 428 429 private static boolean isDuplicateNotification(Context context, NotificationInd nInd) { 430 final byte[] rawLocation = nInd.getContentLocation(); 431 if (rawLocation != null) { 432 String location = new String(rawLocation); 433 String[] selectionArgs = new String[] { location }; 434 Cursor cursor = null; 435 try { 436 cursor = SqliteWrapper.query( 437 context, 438 context.getContentResolver(), 439 Telephony.Mms.CONTENT_URI, 440 new String[]{Telephony.Mms._ID}, 441 LOCATION_SELECTION, 442 new String[]{ 443 Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND), 444 new String(rawLocation) 445 }, 446 null/*sortOrder*/); 447 if (cursor != null && cursor.getCount() > 0) { 448 // We already received the same notification before. 449 return true; 450 } 451 } catch (SQLiteException e) { 452 Rlog.e(TAG, "failed to query existing notification ind", e); 453 } finally { 454 if (cursor != null) { 455 cursor.close(); 456 } 457 } 458 } 459 return false; 460 } 461} 462