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