1 /****************************************************************************
3 ** Copyright (C) 2000-2008 TROLLTECH ASA. All rights reserved.
5 ** This file is part of the Opensource Edition of the Qtopia Toolkit.
7 ** This software is licensed under the terms of the GNU General Public
8 ** License (GPL) version 2.
10 ** See http://www.trolltech.com/gpl/ for GPL licensing information.
12 ** Contact info@trolltech.com if any conditions of this licensing are
17 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
18 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
20 ****************************************************************************/
22 #include <qmodemsmsreader.h>
23 #include <qmodemservice.h>
24 #include <qmodemindicators.h>
25 #include <qatresult.h>
26 #include <qatresultparser.h>
28 #include <qretryatchat.h>
29 #include <qsimenvelope.h>
32 #include <qtopialog.h>
35 \class QModemSMSReader
37 \brief The QModemSMSReader class provides SMS reading facilities for AT-based modems.
38 \ingroup telephony::modem
40 This class uses the \c{AT+CPMS}, \c{AT+CMGL}, \c{AT+CMGR},
41 \c{AT+CMGD}, \c{AT+CNMI}, and \c{AT+CMGF} commands from
44 QModemSMSReader implements the QSMSReader telephony interface. Client
45 applications should use QSMSReader instead of this class to
46 access the modem's incoming SMS message store.
51 class QSMSTaggedMessage
59 class QModemSMSReaderPrivate
62 QModemSMSReaderPrivate()
67 fragmentsOnly = false;
71 pendingFirstMessage = false;
77 QModemService *service;
79 QList<QSMSTaggedMessage *> pending;
80 QList<QSMSTaggedMessage *> pseudoInbox;
88 bool pendingFirstMessage;
91 QStringList unreadList;
95 Create a new modem-based SMS request object for \a service.
97 QModemSMSReader::QModemSMSReader( QModemService *service )
98 : QSMSReader( service->service(), service, QCommInterface::Server )
100 d = new QModemSMSReaderPrivate();
101 d->service = service;
102 connect( service, SIGNAL(resetModem()), this, SLOT(resetModem()) );
103 service->connectToPost( "smsready", this, SLOT(smsReady()) );
105 service->primaryAtChat()->registerNotificationType
106 ( "+CMTI:", this, SLOT(newMessageArrived()) );
107 service->primaryAtChat()->registerNotificationType
108 ( "+CDSI:", this, SLOT(newMessageArrived()) );
110 connect( service->primaryAtChat(),
111 SIGNAL(pduNotification(QString,QByteArray)),
113 SLOT(pduNotification(QString,QByteArray)) );
117 Destroy this modem-based SMS request object.
119 QModemSMSReader::~QModemSMSReader()
127 void QModemSMSReader::check()
135 void QModemSMSReader::firstMessage()
137 if ( d->haveCached ) {
141 d->pendingFirstMessage = true;
150 void QModemSMSReader::nextMessage()
152 if ( !d->unreadList.isEmpty() ) {
153 // Remove messages in the pending queue from the unread list.
154 for ( int posn = 0; posn < d->pending.count(); ++posn )
155 d->unreadList.removeAll( d->pending.at(posn)->identifier );
158 if ( d->pendingPosn < d->pending.count() ) {
160 // Pass the next entry to the "fetched" callback.
161 QSMSTaggedMessage *tmsg = d->pending.at( d->pendingPosn++ );
162 fetched( tmsg->identifier, tmsg->message );
166 // We are at the end of the pending list.
168 QSMSMessage dummyMsg;
169 emit fetched( "", dummyMsg );
177 void QModemSMSReader::deleteMessage( const QString& id )
179 if ( d->unreadList.contains( id ) ) {
180 d->unreadList.removeAll( id );
183 QList<QSMSTaggedMessage *>::Iterator it;
184 for ( it = d->pending.begin(); it != d->pending.end(); ++it ) {
185 if ( (*it)->identifier == id ) {
186 d->pending.erase( it );
190 if ( id.indexOf( QChar(':') ) == 2 ) {
192 // Extract the name of the message store.
193 QString store = id.left(2);
196 // Remove trailing timestamp
197 int timeIndex = trimId.indexOf( QChar(':'), 3 );
199 trimId = trimId.left( timeIndex );
201 // Process the comma-separated indices in the list. Normally
202 // there will be only one, but there could be multiple if
203 // the message was reconstructed from multiple parts.
205 int temp, numDeleted;
207 if ( store != "@@" && !messageStore().isEmpty() )
208 d->service->primaryAtChat()->chat( "AT+CPMS=\"" + store + "\"" );
210 while ( posn < (int)trimId.length() ) {
211 temp = trimId.indexOf( QChar(','), posn );
213 index = trimId.mid( posn );
214 posn = trimId.length();
216 index = trimId.mid( posn, temp - posn );
219 if ( store == "@@" ) {
220 // Delete the message from the pseudo inbox.
221 QList<QSMSTaggedMessage *>::Iterator it;
222 QSMSTaggedMessage *tmsg;
223 QString actualId = "@@:" + index;
224 it = d->pseudoInbox.begin();
225 while ( it != d->pseudoInbox.end() ) {
227 if ( tmsg->identifier == actualId ) {
228 d->pseudoInbox.erase( it );
235 d->service->primaryAtChat()->chat( "AT+CMGD=" + index );
240 // Update the "used messages" count to reflect the change.
241 if ( numDeleted > 0 ) {
242 int used = usedMessages();
246 setValue( "usedMessages", used );
248 // After we have deleted message(s), the SIM is no longer full
249 d->service->indicators()->setSmsMemoryFull( QModemIndicators::SmsMemoryOK );
257 void QModemSMSReader::setUnreadCount( int value )
259 d->unreadCount = value;
263 void QModemSMSReader::updateUnreadCount()
265 QVariant prev = value( "unreadCount" );
266 if ( prev.isNull() || prev.toInt() != (int)(d->unreadCount) ) {
267 setValue( "unreadCount", (int)(d->unreadCount) );
268 emit unreadCountChanged();
272 void QModemSMSReader::updateUnreadList()
274 setValue( "unreadList", d->unreadList );
278 Returns the name of the SMS message store to scan for incoming messages.
279 The default implementation returns \c SM. If this function returns
280 an empty string, then the message store will not be changed from
281 the modem's power-on default with an \c{AT+CPMS} command.
283 \sa messageListCommand()
285 QString QModemSMSReader::messageStore() const
287 return "SM"; // No tr
291 Returns the command that lists the contents of an SMS message store.
292 Normally this is \c{AT+CMGL=4}, but some modems do not support the \c{=4}.
296 QString QModemSMSReader::messageListCommand() const
298 return "AT+CMGL=4"; // No tr
302 Process a SIM download \a message from the network. Modem
303 vendor plug-ins should override this and send the message back
306 Normally SIM download messages are processed internally by the modem.
307 This function is only called if the message ends up in the incoming
310 The default implementation composes a \c{SMS-PP DOWNLOAD}
311 envelope, according to 3GPP TS 11.14, and sends it to the
312 modem using the \c{AT+CSIM command}.
314 void QModemSMSReader::simDownload( const QSMSMessage& message )
316 // Construct the SMS-PP DOWNLOAD envelope to be sent to the SIM.
318 env.setType( QSimEnvelope::SMSPPDownload );
319 QByteArray pdu = message.toPdu();
320 int sclen = (pdu[0] & 0xFF);
321 env.addExtensionField( 0x06, pdu.mid( 1, sclen ) ); // Service center address.
322 env.addExtensionField( 0x8B, pdu.mid( sclen + 1 ) ); // SMS-TPDU for the SMS-DELIVER.
325 // Build the payload for the AT+CSIM command.
331 cmd += (char)pdu.size();
334 // Send the AT+CSIM command to the modem.
335 d->service->primaryAtChat()->chat
336 ( "AT+CSIM=" + QString::number( cmd.size() * 2 ) + "," + QAtUtils::toHex( cmd ) );
339 void QModemSMSReader::check(bool force)
341 if ( d->haveCached || ( d->initializing && !force ) ) {
342 emit messageCount( d->pending.count() );
346 d->pendingCheck = true;
352 void QModemSMSReader::resetModem()
354 if ( !d->initializing ) {
355 d->initializing = true;
356 d->service->post( "needsms" );
360 void QModemSMSReader::smsReady()
362 if ( d->initializing ) {
363 d->initializing = false;
365 // Query the available SMS message notification options.
366 d->service->primaryAtChat()->chat
367 ( "AT+CNMI=?", this, SLOT(nmiStatusReceived(bool,QAtResult)) );
369 // Perform the initial "how many messages are there" check.
374 static QChar cnmiOpt(int flag, const char* pref)
376 for ( ; *pref; pref++ ) {
378 if ( flag & (1 << bit) )
381 return '0'; // not good...
384 void QModemSMSReader::nmiStatusReceived( bool, const QAtResult& result )
386 int flags[5] = {0, 0, 0, 0, 0};
392 QAtResultParser cmd( result );
394 // Collect up flags that represent the supported indication types.
395 if ( cmd.next( "+CNMI:" ) ) {
400 while ( index < 5 && lposn < line.length() ) {
401 ch = line[lposn++].unicode();
404 } else if ( ch == ',' ) {
407 } else if ( ch == ')' ) {
409 } else if ( insideb && ch >= '0' && ch <= '9' ) {
410 if ( lposn < ( line.length() - 1 ) &&
411 line[lposn] == '-' &&
412 line[lposn + 1] >= '0' && line[lposn + 1] <= '9' )
414 ch2 = line[lposn + 1].unicode();
416 while ( ch <= ch2 ) {
417 flags[index] |= (1 << (ch - '0'));
423 flags[index] |= (1 << (ch - '0'));
429 // Construct the command to send to enable indications.
430 // We send ordinary SMS messages into the device's queue, and
431 // cell broadcast messages direct to us.
433 line += cnmiOpt(flags[0],"2310");
435 line += cnmiOpt(flags[1],"10");
437 line += cnmiOpt(flags[2],"2301");
439 line += cnmiOpt(flags[3],"01");
441 line += cnmiOpt(flags[4],"01");
443 // Send the command. If it doesn't work, then we assume that
444 // the phone acts in "won't notify" mode, so no harm done.
446 // Some modems fail to process this command for a small period
447 // of time after the AT+CPIN request at startup, so we need
448 // to repeat the command if it fails.
449 new QRetryAtChat( d->service->primaryAtChat(), line, 25 );
451 // We are now initialized.
452 d->initializing = false;
455 void QModemSMSReader::newMessageArrived()
457 d->fragmentsOnly = false;
459 d->needRefetch = true;
461 d->haveCached = false;
466 void QModemSMSReader::pduNotification
467 ( const QString& type, const QByteArray& pdu )
469 if ( type.startsWith( "+CMT:" ) ) {
471 // This is an SMS message that was delivered directly to Qtopia
472 // without first going through the incoming SMS queue. Technically,
473 // this is a violation of 3GPP TS 07.05 because we explicitly sent a
474 // "AT+CNMI" command to stop this. But some modems do it for SMS
475 // datagrams and WAP Push messages, while still putting text messages
476 // into the normal queue. We add it to the pseudo inbox.
477 QString id = "@@:" + QString::number( d->pseudoIndex++ );
478 QSMSTaggedMessage *tmsg = new QSMSTaggedMessage;
479 tmsg->identifier = id;
480 tmsg->isUnread = true;
481 tmsg->message = QSMSMessage::fromPdu( pdu );
482 d->pseudoInbox.append( tmsg );
484 // Fake out a new message notification to force a check.
490 void QModemSMSReader::extractMessages( const QString& store, const QAtResult& result )
492 QAtResultParser cmd( result );
493 while ( cmd.next( "+CMGL:" ) ) {
495 // Get the message index and the PDU information.
496 uint index = cmd.readNumeric();
497 uint status = cmd.readNumeric();
498 QByteArray pdu = QAtUtils::fromHex( cmd.readNextLine() );
501 QSMSTaggedMessage *tmsg = new QSMSTaggedMessage;
502 tmsg->isUnread = ( status == 0 );
503 tmsg->message = QSMSMessage::fromPdu( pdu );
505 // Build the message identifier from the store, index and date
506 QString id = store + ":" + QString::number( index );
507 id += ":" + QString::number( tmsg->message.timestamp().toTime_t(), 36 );
508 tmsg->identifier = id;
510 // Record the message information for later.
511 d->pending.append( tmsg );
516 void QModemSMSReader::cpmsDone( bool ok, const QAtResult& result )
518 // Look at the used and total values to determine if the
519 // incoming SMS memory store is full or not, so we can update
520 // the "SMSMemoryFull" indicator on modems that don't have
521 // a proprietry way of detecting the full state.
522 QAtResultParser parser( result );
523 QModemIndicators *indicators = d->service->indicators();
524 if ( parser.next( "+CPMS:" ) ) {
525 if ( parser.line().startsWith( QChar('"') ) )
526 parser.readString(); // Skip store name, if present.
527 uint used = parser.readNumeric();
528 uint total = parser.readNumeric();
529 if ( used < total ) {
530 indicators->setSmsMemoryFull( QModemIndicators::SmsMemoryOK );
532 indicators->setSmsMemoryFull( QModemIndicators::SmsMemoryFull );
535 // Update the local value space with the actual counts.
536 setValue( "usedMessages", (int)used, Delayed );
537 setValue( "totalMessages", (int)total );
543 qLog(Modem) << __PRETTY_FUNCTION__ << "Giving up on CPMS.... it keeps failing. how to escalate?";
547 void QModemSMSReader::listMessages()
549 d->service->primaryAtChat()->chat
550 ( messageListCommand(), this, SLOT(storeListDone(bool,QAtResult)) );
553 void QModemSMSReader::storeListDone( bool ok, const QAtResult& result )
556 // ask again... CPMS succeeded so CMGL will work sooner or later too
557 QTimer::singleShot(500, this, SLOT(listMessages()));
561 // Read the messages from the response.
562 QString store = messageStore();
563 if ( store.isEmpty() )
565 extractMessages( store, result );
567 // The SIM is now ready to perform SMS read operations
568 // if the AT+CMGL command was successful.
572 // Join together messages that are sent as a multi-part.
575 // We now have cached messages.
579 // If another message arrived while we were fetching,
580 // then we need to re-fetch the entire list from scratch.
581 if ( d->needRefetch ) {
582 d->needRefetch = false;
587 // Determine what to do with the messages we just fetched.
588 if ( d->pendingCheck ) {
589 d->pendingCheck = false;
590 emit messageCount( d->pending.count() );
594 if ( d->pendingFirstMessage ) {
595 d->pendingFirstMessage = false;
601 void QModemSMSReader::fetchMessages()
603 // Enter fetching mode.
606 // Clear the pending message list.
609 // Copy the pseudo inbox into the pending list because some of
610 // the messages we are looking for may have arrived via "+CMT:".
611 for ( int index = 0; index < d->pseudoInbox.size(); ++index ) {
612 QSMSTaggedMessage *tmsg = new QSMSTaggedMessage;
613 tmsg->identifier = d->pseudoInbox[index]->identifier;
614 tmsg->isUnread = d->pseudoInbox[index]->isUnread;
615 d->pseudoInbox[index]->isUnread = false; // Now it has been read.
616 tmsg->message = d->pseudoInbox[index]->message;
617 d->pending.append( tmsg );
620 // Make sure that PDU mode is enabled. If the device was reset,
621 // then it may have forgotten about PDU mode.
622 d->service->primaryAtChat()->chat( "AT+CMGF=0" );
624 // Select the message store and request the message list.
625 QString store = messageStore();
627 if ( !store.isEmpty() ) {
628 cmd = "AT+CPMS=\"" + store + "\"";
633 QRetryAtChat* chat = new QRetryAtChat(d->service->primaryAtChat(), cmd, 25);
634 connect(chat, SIGNAL(done(bool,QAtResult)), SLOT(cpmsDone(bool,QAtResult)));
637 void QModemSMSReader::joinMessages()
641 // Join the messages together.
642 QStringList toBeDeleted;
643 if ( joinMessages( d->pending, toBeDeleted ) ) {
644 // We only have fragments, so temporarily disable the new
645 // message check until we get something else in the queue.
646 d->fragmentsOnly = true;
649 // Delete any datagrams that were dispatched by "joinMessages".
650 for ( posn = 0; posn < (int)(toBeDeleted.count()); ++posn ) {
651 deleteMessage( toBeDeleted[posn] );
654 // Count the number of unread messages in the combined list.
655 // Add any new ones to the list of unread message identifiers.
656 for ( posn = 0; posn < d->pending.count(); ++posn ) {
657 if ( d->pending.at(posn)->isUnread ) {
659 d->unreadList += d->pending.at(posn)->identifier;
664 // Get the multipart identifier information for a message.
665 static bool getMultipartInfo( const QSMSMessage& msg, QString& multiId,
666 uint& part, uint& numParts )
668 QByteArray headers = msg.headers();
672 while ( ( posn + 2 ) <= headers.size() ) {
673 tag = (unsigned char)(headers[posn]);
674 len = (unsigned char)(headers[posn + 1]);
675 if ( ( posn + len + 2 ) > headers.size() )
677 if ( tag == 0 && len >= 3 ) {
678 // Concatenated message with 8-bit identifiers.
679 multiId = msg.sender() + "&" +
681 ( (uint)(unsigned char)(headers[posn + 2]) );
682 numParts = (unsigned char)(headers[posn + 3]);
683 part = (unsigned char)(headers[posn + 4]);
684 if ( numParts && part && part <= numParts )
686 } else if ( tag == 8 && len >= 4 ) {
687 // Concatenated message with 16-bit identifiers.
688 multiId = msg.sender() + "&" +
690 ( ( (uint)(unsigned char)(headers[posn + 2]) << 8 ) +
691 (uint)(unsigned char)(headers[posn + 3]) );
692 numParts = (unsigned char)(headers[posn + 4]);
693 part = (unsigned char)(headers[posn + 5]);
694 if ( numParts && part && part <= numParts )
702 // Find multipart messages within a list and join them together.
703 // The "messages" list will be modified in-place with the new list.
704 // Returns true if there were left-over fragments that could not
705 // be combined into full messages. This function will also dispatch
706 // datagram messages to their destinations. The "toBeDeleted" list will
707 // contain the identifiers of messages to be deleted because their
708 // corresponding datagrams have been dispatched.
709 bool QModemSMSReader::joinMessages
710 ( QList<QSMSTaggedMessage *>& messages, QStringList& toBeDeleted )
715 uint part2, numParts2;
717 QList<QSMSTaggedMessage *> newList;
718 QSMSTaggedMessage *tmsg;
719 QList<QSMSTaggedMessage> partList;
722 // Construct a new message list. This could probably be made
723 // more efficient, but we normally won't be processing more than
724 // a handful of messages at a time.
726 for ( posn = 0; posn < messages.count(); ++posn ) {
727 if ( getMultipartInfo
728 ( messages.at(posn)->message, multiId, part, numParts ) ) {
730 // Collect up the rest of the parts for this message.
732 partList.append( *messages.at(posn) );
733 for ( posn2 = posn + 1; posn2 < messages.count(); ++posn2 ) {
735 // Skip this message if it isn't part of the same multipart.
736 if ( !getMultipartInfo
737 ( messages.at(posn2)->message,
738 multiId2, part2, numParts2 ) ) {
740 } else if ( multiId != multiId2 ) {
744 // Add the part to the temporary list.
745 partList.append( *messages.at(posn2) );
748 // Do we have all of the parts that we are interested in?
749 if ( partList.count() == (int)numParts ) {
750 tmsg = new QSMSTaggedMessage();
751 tmsg->isUnread = false;
752 tmsg->identifier = messages.at(posn)->identifier.left(3);
753 tmsg->message = messages.at(posn)->message;
754 tmsg->message.setText( "" );
755 tmsg->message.setHeaders( QByteArray() );
756 for ( part = 1; part <= numParts; ++part ) {
757 for ( posn2 = 0; posn2 < partList.count(); ++posn2 ) {
759 ( partList[posn2].message,
760 multiId2, part2, numParts2 );
761 if ( part == part2 ) {
762 if ( partList[posn2].isUnread )
763 tmsg->isUnread = true;
764 tmsg->message.addParts
765 ( partList[posn2].message.parts() );
767 // Remove timestamp from part identifier
768 QString id = partList[posn2].identifier.mid(3);
769 int timeIndex = id.indexOf( QChar(':') );
771 id = id.left( timeIndex );
773 if ( tmsg->identifier.length() == 3 ) {
774 tmsg->identifier += id;
776 tmsg->identifier += "," + id;
781 // Append timestamp to message identifier
782 uint time_t = tmsg->message.timestamp().toTime_t();
783 tmsg->identifier += ":" + QString::number( time_t, 36 );
785 if ( dispatchDatagram( tmsg ) ) {
786 toBeDeleted.append( tmsg->identifier );
789 newList.append( tmsg );
797 // Ordinary message: copy directly to the new list.
798 tmsg = new QSMSTaggedMessage();
799 tmsg->identifier = messages.at(posn)->identifier;
800 tmsg->isUnread = messages.at(posn)->isUnread;
801 tmsg->message = messages.at(posn)->message;
802 if ( dispatchDatagram( tmsg ) ) {
803 toBeDeleted.append( tmsg->identifier );
806 newList.append( tmsg );
815 // Dispatch an SMS message that has an application port number
816 // to the local application that handles that kind of service.
817 // If the port number is not understood, then we pass the
818 // message to the normal message handling program (e.g. qtmail).
819 bool QModemSMSReader::dispatchDatagram( QSMSTaggedMessage *m )
821 // Check for SIM download packets that were not extracted by the modem.
822 if ( m->message.protocol() == 0x3F || // SIM data download
823 m->message.protocol() == 0x3C ) { // ANSI-136 R-DATA
824 if ( ( m->message.dataCodingScheme() & 0x03 ) == 2 ) {
825 // Class 2 message intended for the SIM.
826 simDownload( m->message );
831 // Dispatch using the core dispatch code in QTelephonyService.
832 return d->service->dispatchDatagram( m->message );