GsmSMSDispatcher.java revision baaaca1c6164eff17488a178859295a1d5be9171
1/* 2 * Copyright (C) 2006 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.gsm; 18 19import android.app.Activity; 20import android.app.PendingIntent; 21import android.app.PendingIntent.CanceledException; 22import android.content.Intent; 23import android.os.AsyncResult; 24import android.os.Message; 25import android.os.SystemProperties; 26import android.provider.Telephony.Sms; 27import android.provider.Telephony.Sms.Intents; 28import android.telephony.ServiceState; 29import android.telephony.SmsCbMessage; 30import android.telephony.gsm.GsmCellLocation; 31import android.util.Config; 32import android.util.Log; 33 34import com.android.internal.telephony.BaseCommands; 35import com.android.internal.telephony.CommandsInterface; 36import com.android.internal.telephony.IccUtils; 37import com.android.internal.telephony.SMSDispatcher; 38import com.android.internal.telephony.SmsHeader; 39import com.android.internal.telephony.SmsMessageBase; 40import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; 41import com.android.internal.telephony.TelephonyProperties; 42 43import java.util.ArrayList; 44import java.util.HashMap; 45import java.util.Iterator; 46 47import static android.telephony.SmsMessage.MessageClass; 48 49final class GsmSMSDispatcher extends SMSDispatcher { 50 private static final String TAG = "GSM"; 51 52 private GSMPhone mGsmPhone; 53 54 GsmSMSDispatcher(GSMPhone phone) { 55 super(phone); 56 mGsmPhone = phone; 57 58 ((BaseCommands)mCm).setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null); 59 } 60 61 /** 62 * Called when a status report is received. This should correspond to 63 * a previously successful SEND. 64 * 65 * @param ar AsyncResult passed into the message handler. ar.result should 66 * be a String representing the status report PDU, as ASCII hex. 67 */ 68 protected void handleStatusReport(AsyncResult ar) { 69 String pduString = (String) ar.result; 70 SmsMessage sms = SmsMessage.newFromCDS(pduString); 71 72 int tpStatus = sms.getStatus(); 73 74 if (sms != null) { 75 int messageRef = sms.messageRef; 76 for (int i = 0, count = deliveryPendingList.size(); i < count; i++) { 77 SmsTracker tracker = deliveryPendingList.get(i); 78 if (tracker.mMessageRef == messageRef) { 79 // Found it. Remove from list and broadcast. 80 if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) { 81 deliveryPendingList.remove(i); 82 } 83 PendingIntent intent = tracker.mDeliveryIntent; 84 Intent fillIn = new Intent(); 85 fillIn.putExtra("pdu", IccUtils.hexStringToBytes(pduString)); 86 try { 87 intent.send(mContext, Activity.RESULT_OK, fillIn); 88 } catch (CanceledException ex) {} 89 90 // Only expect to see one tracker matching this messageref 91 break; 92 } 93 } 94 } 95 acknowledgeLastIncomingSms(true, Intents.RESULT_SMS_HANDLED, null); 96 } 97 98 99 /** {@inheritDoc} */ 100 protected int dispatchMessage(SmsMessageBase smsb) { 101 102 // If sms is null, means there was a parsing error. 103 if (smsb == null) { 104 return Intents.RESULT_SMS_GENERIC_ERROR; 105 } 106 SmsMessage sms = (SmsMessage) smsb; 107 boolean handled = false; 108 109 if (sms.isTypeZero()) { 110 // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be 111 // Displayed/Stored/Notified. They should only be acknowledged. 112 Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack"); 113 return Intents.RESULT_SMS_HANDLED; 114 } 115 116 // Special case the message waiting indicator messages 117 if (sms.isMWISetMessage()) { 118 mGsmPhone.updateMessageWaitingIndicator(true); 119 handled = sms.isMwiDontStore(); 120 if (Config.LOGD) { 121 Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled); 122 } 123 } else if (sms.isMWIClearMessage()) { 124 mGsmPhone.updateMessageWaitingIndicator(false); 125 handled = sms.isMwiDontStore(); 126 if (Config.LOGD) { 127 Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled); 128 } 129 } 130 131 if (handled) { 132 return Intents.RESULT_SMS_HANDLED; 133 } 134 135 if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) { 136 // It's a storable message and there's no storage available. Bail. 137 // (See TS 23.038 for a description of class 0 messages.) 138 return Intents.RESULT_SMS_OUT_OF_MEMORY; 139 } 140 141 SmsHeader smsHeader = sms.getUserDataHeader(); 142 // See if message is partial or port addressed. 143 if ((smsHeader == null) || (smsHeader.concatRef == null)) { 144 // Message is not partial (not part of concatenated sequence). 145 byte[][] pdus = new byte[1][]; 146 pdus[0] = sms.getPdu(); 147 148 if (smsHeader != null && smsHeader.portAddrs != null) { 149 if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { 150 return mWapPush.dispatchWapPdu(sms.getUserData()); 151 } else { 152 // The message was sent to a port, so concoct a URI for it. 153 dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); 154 } 155 } else { 156 // Normal short and non-port-addressed message, dispatch it. 157 dispatchPdus(pdus); 158 } 159 return Activity.RESULT_OK; 160 } else { 161 // Process the message part. 162 return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs); 163 } 164 } 165 166 /** {@inheritDoc} */ 167 protected void sendData(String destAddr, String scAddr, int destPort, 168 byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { 169 SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( 170 scAddr, destAddr, destPort, data, (deliveryIntent != null)); 171 sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); 172 } 173 174 /** {@inheritDoc} */ 175 protected void sendText(String destAddr, String scAddr, String text, 176 PendingIntent sentIntent, PendingIntent deliveryIntent) { 177 SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( 178 scAddr, destAddr, text, (deliveryIntent != null)); 179 sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); 180 } 181 182 /** {@inheritDoc} */ 183 protected void sendMultipartText(String destinationAddress, String scAddress, 184 ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, 185 ArrayList<PendingIntent> deliveryIntents) { 186 187 int refNumber = getNextConcatenatedRef() & 0x00FF; 188 int msgCount = parts.size(); 189 int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN; 190 191 for (int i = 0; i < msgCount; i++) { 192 TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false); 193 if (encoding != details.codeUnitSize 194 && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN 195 || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) { 196 encoding = details.codeUnitSize; 197 } 198 } 199 200 for (int i = 0; i < msgCount; i++) { 201 SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); 202 concatRef.refNumber = refNumber; 203 concatRef.seqNumber = i + 1; // 1-based sequence 204 concatRef.msgCount = msgCount; 205 // TODO: We currently set this to true since our messaging app will never 206 // send more than 255 parts (it converts the message to MMS well before that). 207 // However, we should support 3rd party messaging apps that might need 16-bit 208 // references 209 // Note: It's not sufficient to just flip this bit to true; it will have 210 // ripple effects (several calculations assume 8-bit ref). 211 concatRef.isEightBits = true; 212 SmsHeader smsHeader = new SmsHeader(); 213 smsHeader.concatRef = concatRef; 214 215 PendingIntent sentIntent = null; 216 if (sentIntents != null && sentIntents.size() > i) { 217 sentIntent = sentIntents.get(i); 218 } 219 220 PendingIntent deliveryIntent = null; 221 if (deliveryIntents != null && deliveryIntents.size() > i) { 222 deliveryIntent = deliveryIntents.get(i); 223 } 224 225 SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(scAddress, destinationAddress, 226 parts.get(i), deliveryIntent != null, SmsHeader.toByteArray(smsHeader), 227 encoding); 228 229 sendRawPdu(pdus.encodedScAddress, pdus.encodedMessage, sentIntent, deliveryIntent); 230 } 231 } 232 233 /** 234 * Send a multi-part text based SMS which already passed SMS control check. 235 * 236 * It is the working function for sendMultipartText(). 237 * 238 * @param destinationAddress the address to send the message to 239 * @param scAddress is the service center address or null to use 240 * the current default SMSC 241 * @param parts an <code>ArrayList</code> of strings that, in order, 242 * comprise the original message 243 * @param sentIntents if not null, an <code>ArrayList</code> of 244 * <code>PendingIntent</code>s (one for each message part) that is 245 * broadcast when the corresponding message part has been sent. 246 * The result code will be <code>Activity.RESULT_OK<code> for success, 247 * or one of these errors: 248 * <code>RESULT_ERROR_GENERIC_FAILURE</code> 249 * <code>RESULT_ERROR_RADIO_OFF</code> 250 * <code>RESULT_ERROR_NULL_PDU</code>. 251 * @param deliveryIntents if not null, an <code>ArrayList</code> of 252 * <code>PendingIntent</code>s (one for each message part) that is 253 * broadcast when the corresponding message part has been delivered 254 * to the recipient. The raw pdu of the status report is in the 255 * extended data ("pdu"). 256 */ 257 private void sendMultipartTextWithPermit(String destinationAddress, 258 String scAddress, ArrayList<String> parts, 259 ArrayList<PendingIntent> sentIntents, 260 ArrayList<PendingIntent> deliveryIntents) { 261 262 // check if in service 263 int ss = mPhone.getServiceState().getState(); 264 if (ss != ServiceState.STATE_IN_SERVICE) { 265 for (int i = 0, count = parts.size(); i < count; i++) { 266 PendingIntent sentIntent = null; 267 if (sentIntents != null && sentIntents.size() > i) { 268 sentIntent = sentIntents.get(i); 269 } 270 SmsTracker tracker = SmsTrackerFactory(null, sentIntent, null); 271 handleNotInService(ss, tracker); 272 } 273 return; 274 } 275 276 int refNumber = getNextConcatenatedRef() & 0x00FF; 277 int msgCount = parts.size(); 278 int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN; 279 280 for (int i = 0; i < msgCount; i++) { 281 TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false); 282 if (encoding != details.codeUnitSize 283 && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN 284 || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) { 285 encoding = details.codeUnitSize; 286 } 287 } 288 289 for (int i = 0; i < msgCount; i++) { 290 SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); 291 concatRef.refNumber = refNumber; 292 concatRef.seqNumber = i + 1; // 1-based sequence 293 concatRef.msgCount = msgCount; 294 concatRef.isEightBits = false; 295 SmsHeader smsHeader = new SmsHeader(); 296 smsHeader.concatRef = concatRef; 297 298 PendingIntent sentIntent = null; 299 if (sentIntents != null && sentIntents.size() > i) { 300 sentIntent = sentIntents.get(i); 301 } 302 303 PendingIntent deliveryIntent = null; 304 if (deliveryIntents != null && deliveryIntents.size() > i) { 305 deliveryIntent = deliveryIntents.get(i); 306 } 307 308 SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(scAddress, destinationAddress, 309 parts.get(i), deliveryIntent != null, SmsHeader.toByteArray(smsHeader), 310 encoding); 311 312 HashMap<String, Object> map = new HashMap<String, Object>(); 313 map.put("smsc", pdus.encodedScAddress); 314 map.put("pdu", pdus.encodedMessage); 315 316 SmsTracker tracker = SmsTrackerFactory(map, sentIntent, deliveryIntent); 317 sendSms(tracker); 318 } 319 } 320 321 /** {@inheritDoc} */ 322 protected void sendSms(SmsTracker tracker) { 323 HashMap map = tracker.mData; 324 325 byte smsc[] = (byte[]) map.get("smsc"); 326 byte pdu[] = (byte[]) map.get("pdu"); 327 328 Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker); 329 mCm.sendSMS(IccUtils.bytesToHexString(smsc), 330 IccUtils.bytesToHexString(pdu), reply); 331 } 332 333 /** 334 * Send the multi-part SMS based on multipart Sms tracker 335 * 336 * @param tracker holds the multipart Sms tracker ready to be sent 337 */ 338 protected void sendMultipartSms (SmsTracker tracker) { 339 ArrayList<String> parts; 340 ArrayList<PendingIntent> sentIntents; 341 ArrayList<PendingIntent> deliveryIntents; 342 343 HashMap map = tracker.mData; 344 345 String destinationAddress = (String) map.get("destination"); 346 String scAddress = (String) map.get("scaddress"); 347 348 parts = (ArrayList<String>) map.get("parts"); 349 sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents"); 350 deliveryIntents = (ArrayList<PendingIntent>) map.get("deliveryIntents"); 351 352 sendMultipartTextWithPermit(destinationAddress, 353 scAddress, parts, sentIntents, deliveryIntents); 354 355 } 356 357 /** {@inheritDoc} */ 358 protected void acknowledgeLastIncomingSms(boolean success, int result, Message response){ 359 // FIXME unit test leaves cm == null. this should change 360 if (mCm != null) { 361 mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response); 362 } 363 } 364 365 /** {@inheritDoc} */ 366 protected void activateCellBroadcastSms(int activate, Message response) { 367 // Unless CBS is implemented for GSM, this point should be unreachable. 368 Log.e(TAG, "Error! The functionality cell broadcast sms is not implemented for GSM."); 369 response.recycle(); 370 } 371 372 /** {@inheritDoc} */ 373 protected void getCellBroadcastSmsConfig(Message response){ 374 // Unless CBS is implemented for GSM, this point should be unreachable. 375 Log.e(TAG, "Error! The functionality cell broadcast sms is not implemented for GSM."); 376 response.recycle(); 377 } 378 379 /** {@inheritDoc} */ 380 protected void setCellBroadcastConfig(int[] configValuesArray, Message response) { 381 // Unless CBS is implemented for GSM, this point should be unreachable. 382 Log.e(TAG, "Error! The functionality cell broadcast sms is not implemented for GSM."); 383 response.recycle(); 384 } 385 386 private int resultToCause(int rc) { 387 switch (rc) { 388 case Activity.RESULT_OK: 389 case Intents.RESULT_SMS_HANDLED: 390 // Cause code is ignored on success. 391 return 0; 392 case Intents.RESULT_SMS_OUT_OF_MEMORY: 393 return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED; 394 case Intents.RESULT_SMS_GENERIC_ERROR: 395 default: 396 return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR; 397 } 398 } 399 400 /** 401 * Holds all info about a message page needed to assemble a complete 402 * concatenated message 403 */ 404 private static final class SmsCbConcatInfo { 405 private final SmsCbHeader mHeader; 406 407 private final String mPlmn; 408 409 private final int mLac; 410 411 private final int mCid; 412 413 public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) { 414 mHeader = header; 415 mPlmn = plmn; 416 mLac = lac; 417 mCid = cid; 418 } 419 420 @Override 421 public int hashCode() { 422 return mHeader.messageIdentifier * 31 + mHeader.updateNumber; 423 } 424 425 @Override 426 public boolean equals(Object obj) { 427 if (obj instanceof SmsCbConcatInfo) { 428 SmsCbConcatInfo other = (SmsCbConcatInfo)obj; 429 430 // Two pages match if all header attributes (except the page 431 // index) are identical, and both pages belong to the same 432 // location (which is also determined by the scope parameter) 433 if (mHeader.geographicalScope == other.mHeader.geographicalScope 434 && mHeader.messageCode == other.mHeader.messageCode 435 && mHeader.updateNumber == other.mHeader.updateNumber 436 && mHeader.messageIdentifier == other.mHeader.messageIdentifier 437 && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme 438 && mHeader.nrOfPages == other.mHeader.nrOfPages) { 439 return matchesLocation(other.mPlmn, other.mLac, other.mCid); 440 } 441 } 442 443 return false; 444 } 445 446 /** 447 * Checks if this concatenation info matches the given location. The 448 * granularity of the match depends on the geographical scope. 449 * 450 * @param plmn PLMN 451 * @param lac Location area code 452 * @param cid Cell ID 453 * @return true if matching, false otherwise 454 */ 455 public boolean matchesLocation(String plmn, int lac, int cid) { 456 switch (mHeader.geographicalScope) { 457 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: 458 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: 459 if (mCid != cid) { 460 return false; 461 } 462 // deliberate fall-through 463 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE: 464 if (mLac != lac) { 465 return false; 466 } 467 // deliberate fall-through 468 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: 469 return mPlmn != null && mPlmn.equals(plmn); 470 } 471 472 return false; 473 } 474 } 475 476 // This map holds incomplete concatenated messages waiting for assembly 477 private HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = 478 new HashMap<SmsCbConcatInfo, byte[][]>(); 479 480 protected void handleBroadcastSms(AsyncResult ar) { 481 try { 482 byte[][] pdus = null; 483 byte[] receivedPdu = (byte[])ar.result; 484 485 if (Config.LOGD) { 486 for (int i = 0; i < receivedPdu.length; i += 8) { 487 StringBuilder sb = new StringBuilder("SMS CB pdu data: "); 488 for (int j = i; j < i + 8 && j < receivedPdu.length; j++) { 489 int b = receivedPdu[j] & 0xff; 490 if (b < 0x10) { 491 sb.append("0"); 492 } 493 sb.append(Integer.toHexString(b)).append(" "); 494 } 495 Log.d(TAG, sb.toString()); 496 } 497 } 498 499 SmsCbHeader header = new SmsCbHeader(receivedPdu); 500 String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC); 501 GsmCellLocation cellLocation = (GsmCellLocation)mGsmPhone.getCellLocation(); 502 int lac = cellLocation.getLac(); 503 int cid = cellLocation.getCid(); 504 505 if (header.nrOfPages > 1) { 506 // Multi-page message 507 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid); 508 509 // Try to find other pages of the same message 510 pdus = mSmsCbPageMap.get(concatInfo); 511 512 if (pdus == null) { 513 // This it the first page of this message, make room for all 514 // pages and keep until complete 515 pdus = new byte[header.nrOfPages][]; 516 517 mSmsCbPageMap.put(concatInfo, pdus); 518 } 519 520 // Page parameter is one-based 521 pdus[header.pageIndex - 1] = receivedPdu; 522 523 for (int i = 0; i < pdus.length; i++) { 524 if (pdus[i] == null) { 525 // Still missing pages, exit 526 return; 527 } 528 } 529 530 // Message complete, remove and dispatch 531 mSmsCbPageMap.remove(concatInfo); 532 } else { 533 // Single page message 534 pdus = new byte[1][]; 535 pdus[0] = receivedPdu; 536 } 537 538 dispatchBroadcastPdus(pdus); 539 540 // Remove messages that are out of scope to prevent the map from 541 // growing indefinitely, containing incomplete messages that were 542 // never assembled 543 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator(); 544 545 while (iter.hasNext()) { 546 SmsCbConcatInfo info = iter.next(); 547 548 if (!info.matchesLocation(plmn, lac, cid)) { 549 iter.remove(); 550 } 551 } 552 } catch (RuntimeException e) { 553 Log.e(TAG, "Error in decoding SMS CB pdu", e); 554 } 555 } 556 557} 558