CdmaSMSDispatcher.java revision d027329cd6105c05c157e3858a170e4f04eb0e3e
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.cdma; 18 19 20import android.app.Activity; 21import android.app.PendingIntent; 22import android.app.PendingIntent.CanceledException; 23import android.content.ContentValues; 24import android.content.Intent; 25import android.content.SharedPreferences; 26import android.database.Cursor; 27import android.database.SQLException; 28import android.os.AsyncResult; 29import android.os.Message; 30import android.os.SystemProperties; 31import android.preference.PreferenceManager; 32import android.provider.Telephony; 33import android.provider.Telephony.Sms.Intents; 34import android.telephony.SmsManager; 35import android.telephony.SmsMessage.MessageClass; 36import android.util.Config; 37import android.util.Log; 38 39import com.android.internal.telephony.CommandsInterface; 40import com.android.internal.telephony.SMSDispatcher; 41import com.android.internal.telephony.SmsHeader; 42import com.android.internal.telephony.SmsMessageBase; 43import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; 44import com.android.internal.telephony.TelephonyProperties; 45import com.android.internal.telephony.WspTypeDecoder; 46import com.android.internal.telephony.cdma.sms.SmsEnvelope; 47import com.android.internal.telephony.cdma.sms.UserData; 48import com.android.internal.util.HexDump; 49 50import java.io.ByteArrayOutputStream; 51import java.util.ArrayList; 52import java.util.Arrays; 53import java.util.HashMap; 54 55import android.content.res.Resources; 56 57 58final class CdmaSMSDispatcher extends SMSDispatcher { 59 private static final String TAG = "CDMA"; 60 61 private byte[] mLastDispatchedSmsFingerprint; 62 private byte[] mLastAcknowledgedSmsFingerprint; 63 64 private boolean mCheckForDuplicatePortsInOmadmWapPush = Resources.getSystem().getBoolean( 65 com.android.internal.R.bool.config_duplicate_port_omadm_wappush); 66 67 CdmaSMSDispatcher(CDMAPhone phone) { 68 super(phone); 69 } 70 71 /** 72 * Called when a status report is received. This should correspond to 73 * a previously successful SEND. 74 * Is a special GSM function, should never be called in CDMA!! 75 * 76 * @param ar AsyncResult passed into the message handler. ar.result should 77 * be a String representing the status report PDU, as ASCII hex. 78 */ 79 protected void handleStatusReport(AsyncResult ar) { 80 Log.d(TAG, "handleStatusReport is a special GSM function, should never be called in CDMA!"); 81 } 82 83 private void handleCdmaStatusReport(SmsMessage sms) { 84 for (int i = 0, count = deliveryPendingList.size(); i < count; i++) { 85 SmsTracker tracker = deliveryPendingList.get(i); 86 if (tracker.mMessageRef == sms.messageRef) { 87 // Found it. Remove from list and broadcast. 88 deliveryPendingList.remove(i); 89 PendingIntent intent = tracker.mDeliveryIntent; 90 Intent fillIn = new Intent(); 91 fillIn.putExtra("pdu", sms.getPdu()); 92 try { 93 intent.send(mContext, Activity.RESULT_OK, fillIn); 94 } catch (CanceledException ex) {} 95 break; // Only expect to see one tracker matching this message. 96 } 97 } 98 } 99 100 /** {@inheritDoc} */ 101 protected int dispatchMessage(SmsMessageBase smsb) { 102 103 // If sms is null, means there was a parsing error. 104 if (smsb == null) { 105 Log.e(TAG, "dispatchMessage: message is null"); 106 return Intents.RESULT_SMS_GENERIC_ERROR; 107 } 108 109 String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); 110 if (inEcm.equals("true")) { 111 return Activity.RESULT_OK; 112 } 113 114 // See if we have a network duplicate SMS. 115 SmsMessage sms = (SmsMessage) smsb; 116 mLastDispatchedSmsFingerprint = sms.getIncomingSmsFingerprint(); 117 if (mLastAcknowledgedSmsFingerprint != null && 118 Arrays.equals(mLastDispatchedSmsFingerprint, mLastAcknowledgedSmsFingerprint)) { 119 return Intents.RESULT_SMS_HANDLED; 120 } 121 // Decode BD stream and set sms variables. 122 sms.parseSms(); 123 int teleService = sms.getTeleService(); 124 boolean handled = false; 125 126 if ((SmsEnvelope.TELESERVICE_VMN == teleService) || 127 (SmsEnvelope.TELESERVICE_MWI == teleService)) { 128 // handling Voicemail 129 int voicemailCount = sms.getNumOfVoicemails(); 130 Log.d(TAG, "Voicemail count=" + voicemailCount); 131 // Store the voicemail count in preferences. 132 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences( 133 mPhone.getContext()); 134 SharedPreferences.Editor editor = sp.edit(); 135 editor.putInt(CDMAPhone.VM_COUNT_CDMA, voicemailCount); 136 editor.apply(); 137 ((CDMAPhone) mPhone).updateMessageWaitingIndicator(voicemailCount); 138 handled = true; 139 } else if (((SmsEnvelope.TELESERVICE_WMT == teleService) || 140 (SmsEnvelope.TELESERVICE_WEMT == teleService)) && 141 sms.isStatusReportMessage()) { 142 handleCdmaStatusReport(sms); 143 handled = true; 144 } else if ((sms.getUserData() == null)) { 145 if (Config.LOGD) { 146 Log.d(TAG, "Received SMS without user data"); 147 } 148 handled = true; 149 } 150 151 if (handled) { 152 return Intents.RESULT_SMS_HANDLED; 153 } 154 155 if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) { 156 // It's a storable message and there's no storage available. Bail. 157 // (See C.S0015-B v2.0 for a description of "Immediate Display" 158 // messages, which we represent as CLASS_0.) 159 return Intents.RESULT_SMS_OUT_OF_MEMORY; 160 } 161 162 if (SmsEnvelope.TELESERVICE_WAP == teleService) { 163 return processCdmaWapPdu(sms.getUserData(), sms.messageRef, 164 sms.getOriginatingAddress()); 165 } 166 167 // Reject (NAK) any messages with teleservice ids that have 168 // not yet been handled and also do not correspond to the two 169 // kinds that are processed below. 170 if ((SmsEnvelope.TELESERVICE_WMT != teleService) && 171 (SmsEnvelope.TELESERVICE_WEMT != teleService) && 172 (SmsEnvelope.MESSAGE_TYPE_BROADCAST != sms.getMessageType())) { 173 return Intents.RESULT_SMS_UNSUPPORTED; 174 } 175 176 /* 177 * TODO(cleanup): Why are we using a getter method for this 178 * (and for so many other sms fields)? Trivial getters and 179 * setters like this are direct violations of the style guide. 180 * If the purpose is to protect against writes (by not 181 * providing a setter) then any protection is illusory (and 182 * hence bad) for cases where the values are not primitives, 183 * such as this call for the header. Since this is an issue 184 * with the public API it cannot be changed easily, but maybe 185 * something can be done eventually. 186 */ 187 SmsHeader smsHeader = sms.getUserDataHeader(); 188 189 /* 190 * TODO(cleanup): Since both CDMA and GSM use the same header 191 * format, this dispatch processing is naturally identical, 192 * and code should probably not be replicated explicitly. 193 */ 194 195 // See if message is partial or port addressed. 196 if ((smsHeader == null) || (smsHeader.concatRef == null)) { 197 // Message is not partial (not part of concatenated sequence). 198 byte[][] pdus = new byte[1][]; 199 pdus[0] = sms.getPdu(); 200 201 if (smsHeader != null && smsHeader.portAddrs != null) { 202 if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { 203 // GSM-style WAP indication 204 return mWapPush.dispatchWapPdu(sms.getUserData()); 205 } else { 206 // The message was sent to a port, so concoct a URI for it. 207 dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); 208 } 209 } else { 210 // Normal short and non-port-addressed message, dispatch it. 211 dispatchPdus(pdus); 212 } 213 return Activity.RESULT_OK; 214 } else { 215 // Process the message part. 216 return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs); 217 } 218 } 219 220 /** 221 * Processes inbound messages that are in the WAP-WDP PDU format. See 222 * wap-259-wdp-20010614-a section 6.5 for details on the WAP-WDP PDU format. 223 * WDP segments are gathered until a datagram completes and gets dispatched. 224 * 225 * @param pdu The WAP-WDP PDU segment 226 * @return a result code from {@link Telephony.Sms.Intents}, or 227 * {@link Activity#RESULT_OK} if the message has been broadcast 228 * to applications 229 */ 230 protected int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address) { 231 int segment; 232 int totalSegments; 233 int index = 0; 234 int msgType; 235 236 int sourcePort = 0; 237 int destinationPort = 0; 238 239 msgType = pdu[index++]; 240 if (msgType != 0){ 241 Log.w(TAG, "Received a WAP SMS which is not WDP. Discard."); 242 return Intents.RESULT_SMS_HANDLED; 243 } 244 totalSegments = pdu[index++]; // >=1 245 segment = pdu[index++]; // >=0 246 247 // Only the first segment contains sourcePort and destination Port 248 if (segment == 0) { 249 //process WDP segment 250 sourcePort = (0xFF & pdu[index++]) << 8; 251 sourcePort |= 0xFF & pdu[index++]; 252 destinationPort = (0xFF & pdu[index++]) << 8; 253 destinationPort |= 0xFF & pdu[index++]; 254 // Some carriers incorrectly send duplicate port fields in omadm wap pushes. 255 // If configured, check for that here 256 if (mCheckForDuplicatePortsInOmadmWapPush) { 257 if (checkDuplicatePortOmadmWappush(pdu,index)) { 258 index = index + 4; // skip duplicate port fields 259 } 260 } 261 } 262 263 // Lookup all other related parts 264 StringBuilder where = new StringBuilder("reference_number ="); 265 where.append(referenceNumber); 266 where.append(" AND address = ?"); 267 String[] whereArgs = new String[] {address}; 268 269 Log.i(TAG, "Received WAP PDU. Type = " + msgType + ", originator = " + address 270 + ", src-port = " + sourcePort + ", dst-port = " + destinationPort 271 + ", ID = " + referenceNumber + ", segment# = " + segment + "/" + totalSegments); 272 273 byte[][] pdus = null; 274 Cursor cursor = null; 275 try { 276 cursor = mResolver.query(mRawUri, RAW_PROJECTION, where.toString(), whereArgs, null); 277 int cursorCount = cursor.getCount(); 278 if (cursorCount != totalSegments - 1) { 279 // We don't have all the parts yet, store this one away 280 ContentValues values = new ContentValues(); 281 values.put("date", (long) 0); 282 values.put("pdu", HexDump.toHexString(pdu, index, pdu.length - index)); 283 values.put("address", address); 284 values.put("reference_number", referenceNumber); 285 values.put("count", totalSegments); 286 values.put("sequence", segment); 287 values.put("destination_port", destinationPort); 288 289 mResolver.insert(mRawUri, values); 290 291 return Intents.RESULT_SMS_HANDLED; 292 } 293 294 // All the parts are in place, deal with them 295 int pduColumn = cursor.getColumnIndex("pdu"); 296 int sequenceColumn = cursor.getColumnIndex("sequence"); 297 298 pdus = new byte[totalSegments][]; 299 for (int i = 0; i < cursorCount; i++) { 300 cursor.moveToNext(); 301 int cursorSequence = (int)cursor.getLong(sequenceColumn); 302 // Read the destination port from the first segment 303 if (cursorSequence == 0) { 304 int destinationPortColumn = cursor.getColumnIndex("destination_port"); 305 destinationPort = (int)cursor.getLong(destinationPortColumn); 306 } 307 pdus[cursorSequence] = HexDump.hexStringToByteArray( 308 cursor.getString(pduColumn)); 309 } 310 // The last part will be added later 311 312 // Remove the parts from the database 313 mResolver.delete(mRawUri, where.toString(), whereArgs); 314 } catch (SQLException e) { 315 Log.e(TAG, "Can't access multipart SMS database", e); 316 return Intents.RESULT_SMS_GENERIC_ERROR; 317 } finally { 318 if (cursor != null) cursor.close(); 319 } 320 321 // Build up the data stream 322 ByteArrayOutputStream output = new ByteArrayOutputStream(); 323 for (int i = 0; i < totalSegments; i++) { 324 // reassemble the (WSP-)pdu 325 if (i == segment) { 326 // This one isn't in the DB, so add it 327 output.write(pdu, index, pdu.length - index); 328 } else { 329 output.write(pdus[i], 0, pdus[i].length); 330 } 331 } 332 333 byte[] datagram = output.toByteArray(); 334 // Dispatch the PDU to applications 335 switch (destinationPort) { 336 case SmsHeader.PORT_WAP_PUSH: 337 // Handle the PUSH 338 return mWapPush.dispatchWapPdu(datagram); 339 340 default:{ 341 pdus = new byte[1][]; 342 pdus[0] = datagram; 343 // The messages were sent to any other WAP port 344 dispatchPortAddressedPdus(pdus, destinationPort); 345 return Activity.RESULT_OK; 346 } 347 } 348 } 349 350 /** {@inheritDoc} */ 351 protected void sendData(String destAddr, String scAddr, int destPort, 352 byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { 353 SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( 354 scAddr, destAddr, destPort, data, (deliveryIntent != null)); 355 sendSubmitPdu(pdu, sentIntent, deliveryIntent); 356 } 357 358 /** {@inheritDoc} */ 359 protected void sendText(String destAddr, String scAddr, String text, 360 PendingIntent sentIntent, PendingIntent deliveryIntent) { 361 SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( 362 scAddr, destAddr, text, (deliveryIntent != null), null); 363 sendSubmitPdu(pdu, sentIntent, deliveryIntent); 364 } 365 366 /** {@inheritDoc} */ 367 protected void sendMultipartText(String destAddr, String scAddr, 368 ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, 369 ArrayList<PendingIntent> deliveryIntents) { 370 371 /** 372 * TODO(cleanup): There is no real code difference between 373 * this and the GSM version, and hence it should be moved to 374 * the base class or consolidated somehow, provided calling 375 * the proper submitpdu stuff can be arranged. 376 */ 377 378 int refNumber = getNextConcatenatedRef() & 0x00FF; 379 int msgCount = parts.size(); 380 int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN; 381 382 for (int i = 0; i < msgCount; i++) { 383 TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false); 384 if (encoding != details.codeUnitSize 385 && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN 386 || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) { 387 encoding = details.codeUnitSize; 388 } 389 } 390 391 for (int i = 0; i < msgCount; i++) { 392 SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); 393 concatRef.refNumber = refNumber; 394 concatRef.seqNumber = i + 1; // 1-based sequence 395 concatRef.msgCount = msgCount; 396 concatRef.isEightBits = true; 397 SmsHeader smsHeader = new SmsHeader(); 398 smsHeader.concatRef = concatRef; 399 400 PendingIntent sentIntent = null; 401 if (sentIntents != null && sentIntents.size() > i) { 402 sentIntent = sentIntents.get(i); 403 } 404 405 PendingIntent deliveryIntent = null; 406 if (deliveryIntents != null && deliveryIntents.size() > i) { 407 deliveryIntent = deliveryIntents.get(i); 408 } 409 410 UserData uData = new UserData(); 411 uData.payloadStr = parts.get(i); 412 uData.userDataHeader = smsHeader; 413 if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) { 414 uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; 415 } else { // assume UTF-16 416 uData.msgEncoding = UserData.ENCODING_UNICODE_16; 417 } 418 uData.msgEncodingSet = true; 419 420 /* By setting the statusReportRequested bit only for the 421 * last message fragment, this will result in only one 422 * callback to the sender when that last fragment delivery 423 * has been acknowledged. */ 424 SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destAddr, 425 uData, (deliveryIntent != null) && (i == (msgCount - 1))); 426 427 sendSubmitPdu(submitPdu, sentIntent, deliveryIntent); 428 } 429 } 430 431 protected void sendSubmitPdu(SmsMessage.SubmitPdu pdu, 432 PendingIntent sentIntent, PendingIntent deliveryIntent) { 433 if (SystemProperties.getBoolean(TelephonyProperties.PROPERTY_INECM_MODE, false)) { 434 if (sentIntent != null) { 435 try { 436 sentIntent.send(SmsManager.RESULT_ERROR_NO_SERVICE); 437 } catch (CanceledException ex) {} 438 } 439 if (Config.LOGD) { 440 Log.d(TAG, "Block SMS in Emergency Callback mode"); 441 } 442 return; 443 } 444 sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); 445 } 446 447 /** {@inheritDoc} */ 448 protected void sendSms(SmsTracker tracker) { 449 HashMap map = tracker.mData; 450 451 // byte smsc[] = (byte[]) map.get("smsc"); // unused for CDMA 452 byte pdu[] = (byte[]) map.get("pdu"); 453 454 Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker); 455 456 mCm.sendCdmaSms(pdu, reply); 457 } 458 459 /** {@inheritDoc} */ 460 protected void sendMultipartSms (SmsTracker tracker) { 461 Log.d(TAG, "TODO: CdmaSMSDispatcher.sendMultipartSms not implemented"); 462 } 463 464 /** {@inheritDoc} */ 465 protected void acknowledgeLastIncomingSms(boolean success, int result, Message response){ 466 // FIXME unit test leaves cm == null. this should change 467 468 String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); 469 if (inEcm.equals("true")) { 470 return; 471 } 472 473 if (mCm != null) { 474 int causeCode = resultToCause(result); 475 mCm.acknowledgeLastIncomingCdmaSms(success, causeCode, response); 476 477 if (causeCode == 0) { 478 mLastAcknowledgedSmsFingerprint = mLastDispatchedSmsFingerprint; 479 } 480 mLastDispatchedSmsFingerprint = null; 481 } 482 } 483 484 protected void handleBroadcastSms(AsyncResult ar) { 485 // Not supported 486 Log.e(TAG, "Error! Not implemented for CDMA."); 487 } 488 489 private int resultToCause(int rc) { 490 switch (rc) { 491 case Activity.RESULT_OK: 492 case Intents.RESULT_SMS_HANDLED: 493 // Cause code is ignored on success. 494 return 0; 495 case Intents.RESULT_SMS_OUT_OF_MEMORY: 496 return CommandsInterface.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE; 497 case Intents.RESULT_SMS_UNSUPPORTED: 498 return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID; 499 case Intents.RESULT_SMS_GENERIC_ERROR: 500 default: 501 return CommandsInterface.CDMA_SMS_FAIL_CAUSE_ENCODING_PROBLEM; 502 } 503 } 504 505 /** 506 * Optional check to see if the received WapPush is an OMADM notification with erroneous 507 * extra port fields. 508 * - Some carriers make this mistake. 509 * ex: MSGTYPE-TotalSegments-CurrentSegment 510 * -SourcePortDestPort-SourcePortDestPort-OMADM PDU 511 * @param origPdu The WAP-WDP PDU segment 512 * @param index Current Index while parsing the PDU. 513 * @return True if OrigPdu is OmaDM Push Message which has duplicate ports. 514 * False if OrigPdu is NOT OmaDM Push Message which has duplicate ports. 515 */ 516 private boolean checkDuplicatePortOmadmWappush(byte[] origPdu, int index) { 517 index += 4; 518 byte[] omaPdu = new byte[origPdu.length - index]; 519 System.arraycopy(origPdu, index, omaPdu, 0, omaPdu.length); 520 521 WspTypeDecoder pduDecoder = new WspTypeDecoder(omaPdu); 522 int wspIndex = 2; 523 524 // Process header length field 525 if (pduDecoder.decodeUintvarInteger(wspIndex) == false) { 526 return false; 527 } 528 529 wspIndex += pduDecoder.getDecodedDataLength(); // advance to next field 530 531 // Process content type field 532 if (pduDecoder.decodeContentType(wspIndex) == false) { 533 return false; 534 } 535 536 String mimeType = pduDecoder.getValueString(); 537 if (mimeType == null) { 538 int binaryContentType = (int)pduDecoder.getValue32(); 539 if (binaryContentType == WspTypeDecoder.CONTENT_TYPE_B_PUSH_SYNCML_NOTI) { 540 return true; 541 } 542 } 543 return false; 544 } 545} 546