GsmSMSDispatcher.java revision 8503144665894f1b5008f540e0cf36c948c763ef
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.Intents; 27import android.telephony.ServiceState; 28import android.telephony.SmsCbMessage; 29import android.telephony.gsm.GsmCellLocation; 30import android.util.Config; 31import android.util.Log; 32 33import com.android.internal.telephony.BaseCommands; 34import com.android.internal.telephony.CommandsInterface; 35import com.android.internal.telephony.IccUtils; 36import com.android.internal.telephony.SMSDispatcher; 37import com.android.internal.telephony.SmsHeader; 38import com.android.internal.telephony.SmsMessageBase; 39import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; 40import com.android.internal.telephony.TelephonyProperties; 41 42import java.util.ArrayList; 43import java.util.HashMap; 44import java.util.Iterator; 45 46import static android.telephony.SmsMessage.MessageClass; 47 48final class GsmSMSDispatcher extends SMSDispatcher { 49 private static final String TAG = "GSM"; 50 51 private GSMPhone mGsmPhone; 52 53 GsmSMSDispatcher(GSMPhone phone) { 54 super(phone); 55 mGsmPhone = phone; 56 57 ((BaseCommands)mCm).setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null); 58 } 59 60 /** 61 * Called when a status report is received. This should correspond to 62 * a previously successful SEND. 63 * 64 * @param ar AsyncResult passed into the message handler. ar.result should 65 * be a String representing the status report PDU, as ASCII hex. 66 */ 67 @Override 68 protected void handleStatusReport(AsyncResult ar) { 69 String pduString = (String) ar.result; 70 SmsMessage sms = SmsMessage.newFromCDS(pduString); 71 72 if (sms != null) { 73 int messageRef = sms.messageRef; 74 for (int i = 0, count = deliveryPendingList.size(); i < count; i++) { 75 SmsTracker tracker = deliveryPendingList.get(i); 76 if (tracker.mMessageRef == messageRef) { 77 // Found it. Remove from list and broadcast. 78 deliveryPendingList.remove(i); 79 PendingIntent intent = tracker.mDeliveryIntent; 80 Intent fillIn = new Intent(); 81 fillIn.putExtra("pdu", IccUtils.hexStringToBytes(pduString)); 82 try { 83 intent.send(mContext, Activity.RESULT_OK, fillIn); 84 } catch (CanceledException ex) {} 85 86 // Only expect to see one tracker matching this messageref 87 break; 88 } 89 } 90 } 91 acknowledgeLastIncomingSms(true, Intents.RESULT_SMS_HANDLED, null); 92 } 93 94 95 /** {@inheritDoc} */ 96 @Override 97 protected int dispatchMessage(SmsMessageBase smsb) { 98 99 // If sms is null, means there was a parsing error. 100 if (smsb == null) { 101 return Intents.RESULT_SMS_GENERIC_ERROR; 102 } 103 SmsMessage sms = (SmsMessage) smsb; 104 boolean handled = false; 105 106 if (sms.isTypeZero()) { 107 // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be 108 // Displayed/Stored/Notified. They should only be acknowledged. 109 Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack"); 110 return Intents.RESULT_SMS_HANDLED; 111 } 112 113 // Special case the message waiting indicator messages 114 if (sms.isMWISetMessage()) { 115 mGsmPhone.updateMessageWaitingIndicator(true); 116 handled = sms.isMwiDontStore(); 117 if (Config.LOGD) { 118 Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled); 119 } 120 } else if (sms.isMWIClearMessage()) { 121 mGsmPhone.updateMessageWaitingIndicator(false); 122 handled = sms.isMwiDontStore(); 123 if (Config.LOGD) { 124 Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled); 125 } 126 } 127 128 if (handled) { 129 return Intents.RESULT_SMS_HANDLED; 130 } 131 132 if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) { 133 // It's a storable message and there's no storage available. Bail. 134 // (See TS 23.038 for a description of class 0 messages.) 135 return Intents.RESULT_SMS_OUT_OF_MEMORY; 136 } 137 138 SmsHeader smsHeader = sms.getUserDataHeader(); 139 // See if message is partial or port addressed. 140 if ((smsHeader == null) || (smsHeader.concatRef == null)) { 141 // Message is not partial (not part of concatenated sequence). 142 byte[][] pdus = new byte[1][]; 143 pdus[0] = sms.getPdu(); 144 145 if (smsHeader != null && smsHeader.portAddrs != null) { 146 if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { 147 return mWapPush.dispatchWapPdu(sms.getUserData()); 148 } else { 149 // The message was sent to a port, so concoct a URI for it. 150 dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); 151 } 152 } else { 153 // Normal short and non-port-addressed message, dispatch it. 154 dispatchPdus(pdus); 155 } 156 return Activity.RESULT_OK; 157 } else { 158 // Process the message part. 159 return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs); 160 } 161 } 162 163 /** {@inheritDoc} */ 164 @Override 165 protected void sendData(String destAddr, String scAddr, int destPort, 166 byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { 167 SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( 168 scAddr, destAddr, destPort, data, (deliveryIntent != null)); 169 sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); 170 } 171 172 /** {@inheritDoc} */ 173 @Override 174 protected void sendText(String destAddr, String scAddr, String text, 175 PendingIntent sentIntent, PendingIntent deliveryIntent) { 176 SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( 177 scAddr, destAddr, text, (deliveryIntent != null)); 178 sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); 179 } 180 181 /** {@inheritDoc} */ 182 @Override 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 @Override 323 protected void sendSms(SmsTracker tracker) { 324 HashMap<String, Object> map = tracker.mData; 325 326 byte smsc[] = (byte[]) map.get("smsc"); 327 byte pdu[] = (byte[]) map.get("pdu"); 328 329 Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker); 330 mCm.sendSMS(IccUtils.bytesToHexString(smsc), 331 IccUtils.bytesToHexString(pdu), reply); 332 } 333 334 /** 335 * Send the multi-part SMS based on multipart Sms tracker 336 * 337 * @param tracker holds the multipart Sms tracker ready to be sent 338 */ 339 @Override 340 protected void sendMultipartSms (SmsTracker tracker) { 341 ArrayList<String> parts; 342 ArrayList<PendingIntent> sentIntents; 343 ArrayList<PendingIntent> deliveryIntents; 344 345 HashMap<String, Object> map = tracker.mData; 346 347 String destinationAddress = (String) map.get("destination"); 348 String scAddress = (String) map.get("scaddress"); 349 350 parts = (ArrayList<String>) map.get("parts"); 351 sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents"); 352 deliveryIntents = (ArrayList<PendingIntent>) map.get("deliveryIntents"); 353 354 sendMultipartTextWithPermit(destinationAddress, 355 scAddress, parts, sentIntents, deliveryIntents); 356 357 } 358 359 /** {@inheritDoc} */ 360 @Override 361 protected void acknowledgeLastIncomingSms(boolean success, int result, Message response){ 362 // FIXME unit test leaves cm == null. this should change 363 if (mCm != null) { 364 mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response); 365 } 366 } 367 368 /** {@inheritDoc} */ 369 @Override 370 protected void activateCellBroadcastSms(int activate, Message response) { 371 // Unless CBS is implemented for GSM, this point should be unreachable. 372 Log.e(TAG, "Error! The functionality cell broadcast sms is not implemented for GSM."); 373 response.recycle(); 374 } 375 376 /** {@inheritDoc} */ 377 @Override 378 protected void getCellBroadcastSmsConfig(Message response){ 379 // Unless CBS is implemented for GSM, this point should be unreachable. 380 Log.e(TAG, "Error! The functionality cell broadcast sms is not implemented for GSM."); 381 response.recycle(); 382 } 383 384 /** {@inheritDoc} */ 385 @Override 386 protected void setCellBroadcastConfig(int[] configValuesArray, Message response) { 387 // Unless CBS is implemented for GSM, this point should be unreachable. 388 Log.e(TAG, "Error! The functionality cell broadcast sms is not implemented for GSM."); 389 response.recycle(); 390 } 391 392 private int resultToCause(int rc) { 393 switch (rc) { 394 case Activity.RESULT_OK: 395 case Intents.RESULT_SMS_HANDLED: 396 // Cause code is ignored on success. 397 return 0; 398 case Intents.RESULT_SMS_OUT_OF_MEMORY: 399 return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED; 400 case Intents.RESULT_SMS_GENERIC_ERROR: 401 default: 402 return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR; 403 } 404 } 405 406 /** 407 * Holds all info about a message page needed to assemble a complete 408 * concatenated message 409 */ 410 private static final class SmsCbConcatInfo { 411 private final SmsCbHeader mHeader; 412 413 private final String mPlmn; 414 415 private final int mLac; 416 417 private final int mCid; 418 419 public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) { 420 mHeader = header; 421 mPlmn = plmn; 422 mLac = lac; 423 mCid = cid; 424 } 425 426 @Override 427 public int hashCode() { 428 return mHeader.messageIdentifier * 31 + mHeader.updateNumber; 429 } 430 431 @Override 432 public boolean equals(Object obj) { 433 if (obj instanceof SmsCbConcatInfo) { 434 SmsCbConcatInfo other = (SmsCbConcatInfo)obj; 435 436 // Two pages match if all header attributes (except the page 437 // index) are identical, and both pages belong to the same 438 // location (which is also determined by the scope parameter) 439 if (mHeader.geographicalScope == other.mHeader.geographicalScope 440 && mHeader.messageCode == other.mHeader.messageCode 441 && mHeader.updateNumber == other.mHeader.updateNumber 442 && mHeader.messageIdentifier == other.mHeader.messageIdentifier 443 && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme 444 && mHeader.nrOfPages == other.mHeader.nrOfPages) { 445 return matchesLocation(other.mPlmn, other.mLac, other.mCid); 446 } 447 } 448 449 return false; 450 } 451 452 /** 453 * Checks if this concatenation info matches the given location. The 454 * granularity of the match depends on the geographical scope. 455 * 456 * @param plmn PLMN 457 * @param lac Location area code 458 * @param cid Cell ID 459 * @return true if matching, false otherwise 460 */ 461 public boolean matchesLocation(String plmn, int lac, int cid) { 462 switch (mHeader.geographicalScope) { 463 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: 464 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: 465 if (mCid != cid) { 466 return false; 467 } 468 // deliberate fall-through 469 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE: 470 if (mLac != lac) { 471 return false; 472 } 473 // deliberate fall-through 474 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: 475 return mPlmn != null && mPlmn.equals(plmn); 476 } 477 478 return false; 479 } 480 } 481 482 // This map holds incomplete concatenated messages waiting for assembly 483 private HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = 484 new HashMap<SmsCbConcatInfo, byte[][]>(); 485 486 protected void handleBroadcastSms(AsyncResult ar) { 487 try { 488 byte[][] pdus = null; 489 byte[] receivedPdu = (byte[])ar.result; 490 491 if (Config.LOGD) { 492 for (int i = 0; i < receivedPdu.length; i += 8) { 493 StringBuilder sb = new StringBuilder("SMS CB pdu data: "); 494 for (int j = i; j < i + 8 && j < receivedPdu.length; j++) { 495 int b = receivedPdu[j] & 0xff; 496 if (b < 0x10) { 497 sb.append("0"); 498 } 499 sb.append(Integer.toHexString(b)).append(" "); 500 } 501 Log.d(TAG, sb.toString()); 502 } 503 } 504 505 SmsCbHeader header = new SmsCbHeader(receivedPdu); 506 String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC); 507 GsmCellLocation cellLocation = (GsmCellLocation)mGsmPhone.getCellLocation(); 508 int lac = cellLocation.getLac(); 509 int cid = cellLocation.getCid(); 510 511 if (header.nrOfPages > 1) { 512 // Multi-page message 513 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid); 514 515 // Try to find other pages of the same message 516 pdus = mSmsCbPageMap.get(concatInfo); 517 518 if (pdus == null) { 519 // This it the first page of this message, make room for all 520 // pages and keep until complete 521 pdus = new byte[header.nrOfPages][]; 522 523 mSmsCbPageMap.put(concatInfo, pdus); 524 } 525 526 // Page parameter is one-based 527 pdus[header.pageIndex - 1] = receivedPdu; 528 529 for (int i = 0; i < pdus.length; i++) { 530 if (pdus[i] == null) { 531 // Still missing pages, exit 532 return; 533 } 534 } 535 536 // Message complete, remove and dispatch 537 mSmsCbPageMap.remove(concatInfo); 538 } else { 539 // Single page message 540 pdus = new byte[1][]; 541 pdus[0] = receivedPdu; 542 } 543 544 dispatchBroadcastPdus(pdus); 545 546 // Remove messages that are out of scope to prevent the map from 547 // growing indefinitely, containing incomplete messages that were 548 // never assembled 549 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator(); 550 551 while (iter.hasNext()) { 552 SmsCbConcatInfo info = iter.next(); 553 554 if (!info.matchesLocation(plmn, lac, cid)) { 555 iter.remove(); 556 } 557 } 558 } catch (RuntimeException e) { 559 Log.e(TAG, "Error in decoding SMS CB pdu", e); 560 } 561 } 562 563} 564