Communications
From UIQ Books
Communications
Mobile phones by their very nature incorporate a wide range of communications technologies. In this chapter we present an overview of these technologies and their characteristics. We then provide example code demonstrating these types of communication:
- sockets
- Bluetooth sockets, together with more information on using sockets in general
- HTTP
- messaging including Send as
- telephony.
Communications Technologies
Wide Area Networking
Current UIQ 3 mobile phones support 2G (GSM) and 3G (UMTS) communications. We show how to perform data communication using sockets and give an introduction to managing voice calls.
Circuit Switched
A modem and landline is the most basic way to connect to the Internet and company networks. The modem establishes a telephone call to the remote server which is circuit-switched, meaning that a telephone circuit is maintained for the duration of the connection, irrespective of whether data flows or not. The connection has a fixed bandwidth and is normally charged on a connected-time basis.
In mobile networks, this service is known as Circuit-Switched Data (CSD) and runs at a basic speed of 9.6kbps, though 14.4kbps is frequently supported. High Speed Circuit Switched Data (HSCSD) is supported by a limited number of mobile operators and provides faster connections. The Sony Ericsson P1i supports HSCSD at 28.8kbps downlink and 14.4kbps uplink.
CSD and HSCSD are rarely used in modern solutions although UIQ 3 still allows CSD and HSCSD Internet Access Points (IAP) to be configured and used.
Packet Switched
Packet-switch methods provide faster connections and much more efficient utilization of network resources. Data is transmitted in small packets, as needed, much like IP on the Internet. Capacity is only used when data is being sent or received, which means that it is possible to be constantly connected so that applications have immediate access to networked servers. The service is typically charged by the amount of data transferred, although unlimited subscriptions (subject to a fair use policy) are increasingly common.
GPRS (General Packet Radio Service) is the original implementation in GSM networks. EDGE (Enhanced Data Rates for Global Evolution) introduced greater throughput using more advanced modulation techniques.
UMTS (Universal Mobile Telecommunications System) networks typically launched with a 384kbps connection speed. High Speed Packet Access (HSPA) implements a high speed shared channel in UMTS. Initially available in the downlink direction only, HSDPA technology increases the downlink speed to 3.6Mbps, 7.2Mbps and up to 14.4Mbps in the future. Complementary HSUPA technology increases the uplink speed from the basic 384kbps up to 5.76Mbps.
| Technology | Downlink | Uplink |
| GPRS (CS-2) | 53.6kbps | 26.8kbps |
| EDGE | 247.4kbps | 123.7kbps |
| UMTS | 384kbps | 384kbps |
| HSDPA | Up to 14.4Mbps | 384kbps |
| HSUPA | Up to 14.4Mbps | Up to 5.76Mbps |
At the time of writing, most HSDPA phones and networks offer 3.6Mbps, while HSUPA is just being introduced at 2Mbps. All HSUPA phones are expected to include HSDPA, since downlink speed is almost always more important to the end user than uplink. The MOTO Z8 supports HSDPA at 3.6Mbps.
A mobile phone normally operates at the best available data rate, which may be constrained by the subscription, coverage and network traffic. Your application simply selects a packet-switched IAP.
Mobile networks are typically configured to transfer a data connection from UMTS/HSPA to GPRS/EDGE if the mobile phone moves out of UMTS coverage and in to GSM-only coverage. The connection will return to UMTS/HSPA if the network and the mobile phone are configured to support the transition back again. The allocated IP number and Domain Name Resolution (DNS) servers remain the same irrespective of which network is serving the mobile phone.
Latency
Latency may be measured as the Round Trip Time (RTT) for a data packet from the mobile phone to be sent to a server on the network and a response packet arriving back. The Ping utility performs such a test. For example, pinging www.sonyericsson.com via a GPRS connection has an RTT around 750ms, depending on network traffic.
EDGE, UMTS and HSPA each introduce additional network features to improve RTT. The same ping test using UMTS has an RTT of around 200ms, while HSDPA reduces it to around 120ms.
Access Point Names
The required service is selected using an Access Point Name (APN). APNs are defined by the mobile operator and are frequently pre-configured on the mobile phone. Many mobile operators configure separate APNs for Internet, MMS services and WAP browsing. It is possible for multiple connections to be in progress at the same time; such devices are said to be multi-homing.
Many applications require access to the Internet, and can select the appropriate connection using the RConnection API.
Internet Access
Mobile operators frequently place limits on Internet access:
- limited access to domains, pages within those domains or content within pages in so called walled gardens
- content filtering to prevent children accessing unsuitable material
- restriction of certain protocols; for example SMTP to external servers may be barred in order to protect against spammers.
IP Addresses
In a mobile network, the IP address of a device is usually dynamically allocated. This is very similar to IP address allocation within a private company LAN via a DHCP server. Therefore, each time you connect, you are likely to be allocated a different IP address.
Network Address Translation (NAT)
Most networks are still using IPv4 and must therefore manage the limited number of available IP addresses. They typically employ network address translation (NAT) to reduce the need for globally unique IP addresses. A typical network will use addresses from the private IP address ranges such as 192.168.x.x or 10.x.x.x. When a device on the local network communicates with the global Internet, the NAT translates the local IP address to a global IP address.
To perform the translation, the NAT needs to maintain mapping tables between the local and global addresses, otherwise it would not be able to route any returning content to the request originator. NAT mappings are automatically created and destroyed without the user knowing. Common inactivity timeouts for NAT mappings are 40-180 seconds for UDP mappings and 30-60 minutes for TCP mappings.
The NAT mapping tables are created on uplink packets from devices, that is, the mapping is created when a phone within the mobile network attempts to communicate with the outside world. Therefore, a server outside the mobile network is unable to initiate connections to mobile phones within the mobile network, even if it knows the local IP address that has been allocated to a phone, for example though a phone sending the server an SMS containing its IP address. In practice, communication must be initiated by the mobile phone unless your application’s server component resides inside the mobile operator’s network.
You should also note that some network protocols have specific requirements if it is expected that NAT will be involved. The most significant of such protocols is an IPSec based virtual private network (VPN) which will not work unless the network layers are enclosed in UDP packets.
Always on connections
If you are building an application that presents an always on metaphor to the user, such as a push Email solution, various aspects of a mobile network and phone usage should be considered:
- Applications should not assume the connection will always remain open. Inactivity timeouts or loss of radio signal may occur in the network and connections may be interrupted when the mobile phone has no network coverage.
- Some phones may have flight modes where no radio activity is possible even though the phone is switched on and otherwise operating normally.
- Applications must be designed to be battery friendly, allowing a system to be in a power save mode for as much of the time as possible. Background activity should be carefully considered and minimized.
- If an application uses polling techniques to keep alive a connection, you should consider the effect that various poll rates have on data volumes and battery life compared to the usefulness of keeping a connection alive.
- Other communications activity on the mobile phone may suspend data connectivity. When on GSM, a voice call will cause any GPRS connection to be suspended.
Roaming
When roaming on another mobile network, there is generally no need to change any settings. The APN of the home network still connects to the same services. The connection is established from the roaming network back through the home network. IP number and DNS are allocated from the home network.
Personal and Local Area Networking
Infrared and Bluetooth are short range communication technologies that are typically used to connect two mobile phones. Some UIQ 3 phones include Wireless LAN compliant with the popular IEEE 802.11 standard.
Infrared
Infrared provides communications over a short distance, about one metre maximum, with a clear line of sight between the two devices. Speeds up to 115kbps are possible on UIQ 3 mobile phones. The protocol stack used is IrDA, standardized by the Infrared Data Association.
Infrared is most commonly used to transfer, or beam, contact details and appointments from one mobile phone to another. The communication is easily set up and does not cost anything.
OBEX, Serial and Socket communication are supported.
Bluetooth Technology
Bluetooth is a wireless technology operating in the 2.4GHz licence-free radio band, often called the ISM or Industrial, Scientific and Medical band. Most phones have a range of up to 10 metres, though 100 metres is also possible. Typical speed is 768kbps, but can be increased up to 2.1Mbps using EDR (Enhanced Data Rate).
Bluetooth technology defines a way of discovering and connecting to other devices. Bluetooth profiles indicate the services that are supported. Dial-Up Networking profile enables a Bluetooth equipped laptop to connect to the Internet using the mobile phone as a modem. Headset profile enables the mobile phone to work with a Bluetooth headset. Communication over Bluetooth technology is free.
OBEX, Serial and Socket communication are supported.
In the Bluetooth Technology section we provide an example where we open sockets over Bluetooth technology and send text strings.
Wireless LAN
Wireless LAN (WLAN) is a common way of connecting to corporate networks and to the Internet. The Sony Ericsson P990i and P1i phones include WLAN to the IEEE 802.11b standard, part of the Wi-Fi family. IEEE 802.11b provides connectivity up to 11Mbps and operates in the same 2.4GHz ISM band as Bluetooth technology. The connection speed drops to 5.5, 2 and 1Mbps as the mobile phone moves away from the Access Point. Maximum range is approximately 100 metres indoors and 400 metres outdoors, depending on obstacles and interference. Additional protocols are used for authentication and encryption.
WLAN is commonly used to make a home ADSL (broadband Internet) connection available to multiple computers. WEP (Wireless Equivalent Privacy) or WPA-Personal (Wireless Protected Access, Personal) is generally enabled to provide authentication of users connecting to the WLAN and encryption of the data packets. To connect, you must know the SSID (Service Set ID) and the network password. Since the cost of the broadband connection is typically fixed and does not increase as more devices are added via WLAN, many users regard such access from a mobile phone to be free, not least because GPRS type packet data charges are avoided.
Internet hotspots in cafes, hotels and airports generally do not employ any authentication at WLAN level. Simply discovering and connecting to the SSID is sufficient. However, without further authentication, connectivity may be strictly limited to, say the service provider’s and the hotel’s website. Full Internet access is gained after entering a username or password, or buying a fixed period of access from the service provider. A default webpage is provided for the user to buy access or log on. Since there is typically no encryption, any sensitive transaction must be protected at a higher level such as secure web pages.
When deployed for corporate LAN access, WPA is used together with a defined authentication technique. Configuration information is required from the network administrator.
When connecting to the Internet from home or a hotspot, NAT is used to manage the IP addresses, so you will meet the same issues as we described for GPRS connections, including those for always on type applications.
WLAN is IP-based, therefore, the Sockets examples in this chapter apply.
Multi-homing
Multi-homing is the ability of a mobile phone to manage multiple connections at once. The need arises as mobile operators typically segregate access to their MMS, WAP and Internet services. The mobile phone must connect to each service separately.
In older GPRS phones, where only one connection could be in progress at once, the connections were prioritized so that, say, an MMS connection would suspend any ongoing Internet connection.
UMTS phones are typically capable of establishing more than one connection at once. The addition of Bluetooth PAN and WLAN gives rise to the possibility of additional concurrent connections. Applications need to select which connection they want to use.
In UIQ 3, connections are defined as IAPs. When you open a socket, you must select the required IAP. You can do this in a number of ways:
- take the user-configured default in UIQ 3
- let UIQ 3 present a list of IAPs to the user at connect time
- select a particular connection, for example, defined in the user preferences for your application.
As a mobile phone moves around, the available networks (especially PAN and WLAN) change. Your application may wish to manage this situation and:
- support specific change requests from a user
- propose changes to a user if a change is detected
- automatically change behind the scenes, for example, via some kind of configurable change policy.
Detecting changes is more complicated but may offer your users a superior experience. You can use the RConnection API to either determine the current IAP availability or receive notifications about changes in bearer availability. Your application must take in to account that the IP number and DNS servers will change when switching between Bluetooth PAN, WLAN and the mobile network.
Messaging-based Communication
Messaging can be used as a transport to convey communication, including specifically coded messages that are sent and received by applications rather than by humans. UIQ 3 provides a Send as interface, which we already used in SignedAppPhase2 to send files. In this chapter we provide a further Send as example and also show you how to scan the inbox for messages and send messages directly.
SMS
A single SMS message can convey 160 seven-bit characters, 140 eight-bit characters or 70 16-bit characters. SMS messages are typically charged per message, though the popularity of the medium means that many users buy a set number of messages per month at a reduced rate. Longer messages are possible using concatenated SMS, where the message is broken in to a number of underlying SMS blocks, and these are reassembled to recreate the full message at the far end. The sender is charged for each underlying block.
MMS
MMS enables the user to send text, audio, images and video from the mobile phone. The service is charged per message and the typical maximum size of the content that is conveyed is in the range 100 to 300kbytes.
POP3, IMAP and SMTP Internet Email accounts can be configured and used on UIQ 3 mobile phones. Both body text and attachments can be conveyed using Email messages.
Voice
Applications can set up and manage voice calls. The ETel ISV API enables your application to monitor phone status and manage calls.
Symbian OS Communications Architecture
ESOCK
ESOCK provides the framework for accessing various lower level protocols in Symbian OS. It provides the sockets API for making connections and the connection management API for selecting, monitoring and configuring connections.
Figure 1 ESOCK architecture
IP network access is possible over several bearers, including Bluetooth, infrared, WLAN, GPRS and CSD type connections. It is also possible for multiple connections to be in progress at the same time, for example the MMS messaging server connecting over UMTS to the network operator MMS server at the same time that an Internet connection is in progress for web browsing.
UIQ 3 provides the GUI for setting up the required parameters for each connection. IAPs can also be preconfigured or set up remotely. The IAP parameters are stored in the communications database CommsDat. An application called Connection Manager allows advanced users to monitor and control connections.
Symbian Signed Requirements
If your application incorporates a communications technology within its feature set, it is very likely to require some capabilities. For example, applications that wish to transfer data using infrared or Bluetooth technology require the LocalServices capability. Applications wishing to communicate using mobile network services such as GPRS or SMS require NetworkServices. If you want to use the messaging architecture, you are likely to need ReadUserData and WriteUserData capabilities.
These capabilities can be granted by a user upon application installation but we recommend that commercial applications are Symbian Signed so the user does not have to worry about granting capabilities. As we discuss in Building an application chapter, this has numerous implications with respect to application UIDs, file naming and Test Criteria conformance.
Although unlikely, it is possible that your application may require NetworkControl capabilities, for example you need to configure the network interface. At the time of writing, this is a Device Manufacturer capability and so you should determine whether you will be granted the required platform approval before proceeding with your application development. In the new Open Signed and Certified Signed schemes, you can get NetworkControl without manufacturer approval. Symbian Signed is explained in detail in the Symbian Signed chapter.
We have not designed the example applications in this chapter to be Symbian Signed. In general, they are not complete applications, but rather focus on demonstrating each topic.
You should be aware that debugging communications applications is difficult. This is partly due to their real-time nature and partly due to the differences between a development PC and a real mobile phone. A common technique is to log information to a file or the UI. Our example applications simply log information to a standard UIQ list box, which is then displayed as the primary UI element. For some types of problem solving, you may find it useful to run your application in the emulator and use PC-based analysis tools such as Wireshark.
Sockets
Sockets provide a high level interface to various communications protocols, allowing the transfer of information between endpoints. While originally developed to support the TCP/IP protocol, the concepts can be also applied to other communications protocols. In Symbian OS, sockets are used for TCP, UDP, Bluetooth and infrared communications.
A socket represents one end of a connection between applications. Usually the applications reside on different devices. In that case, data written to a socket on one device can be read from a socket on the remote device.
Connected and Connectionless Sockets
A socket is either classified as connected (or connection oriented) or connectionless. For connected sockets, a path between the two endpoints must be established. Once a connection is established, data can flow between the endpoints without having to know the address of the remote endpoint every transaction. Data flow is usually ordered and delivery is deterministic. On the other hand, a connectionless socket forms the path between the endpoints for every item of data transferred; data flow is not normally ordered and often little knowledge of the successful delivery or otherwise is known. TCP implements connected sockets, UDP uses connectionless sockets.
ESOCK API
Socket based communications in Symbian OS utilize the following classes:
RSocketServ
This represents the connection between the client application and the server process within Symbian OS that actually implements the communication protocols.
This class provides:
- general enquiry functions about the available communications protocols
- the mechanism by which communication sub sessions are instantiated.
RHostResolver
This is responsible for name resolution services. If a protocol supports a DNS service you use a RHostResolver class to gain access to those services.
RSocket
This represents the communications protocol endpoint.
RConnection
This is responsible for managing the network interface - in effect the transport layer.
RNetDatabase
Some protocols have a database associated with them, for example LM-IAS with the infrared protocol. This class enables access to such databases.
RSocketServ
Before any socket-based communications can take place you need to establish an IPC connection to the Symbian OS socket server process. RSocketServ is the client side object encapsulating your IPC connection to that process. Once the IPC connection is established you can make requests, for example, to enumerate the communication protocols available or create an endpoint.
The Sockets1 example application enumerates the available communications protocols and displays them as a list box:
void CAppSpecificListView::ViewConstructL()
{
ViewConstructFromResourceL(R_APP_VIEW_CONFIGURATIONS);
User::LeaveIfError(iSocketServ.Connect());
CQikListBox* listbox=LocateControlByUniqueHandle<CQikListBox>(EAppSpecificListViewListId);
MQikListBoxModel& model(listbox->Model());
model.ModelBeginUpdateLC();
TBuf<128>bb;
TUint count;
User::LeaveIfError(iSocketServ.NumProtocols(count));
TProtocolDesc info;
for (TUint i=0;i<count;i++)
{
User::LeaveIfError(iSocketServ.GetProtocolInfo(i+1,info));
MQikListBoxData* lbData=model.NewDataL(MQikListBoxModel::EDataNormal);
CleanupClosePushL(*lbData);
lbData->AddTextL(info.iName,EQikListBoxSlotText1);
_LIT(KTypeProtAddr,"Type:%u, Prot:%u, Addr:%u");
bb.Format(KTypeProtAddr,info.iSockType,info.iProtocol,info.iAddrFamily);
lbData->AddTextL(bb,EQikListBoxSlotText2);
CleanupStack::PopAndDestroy(lbData);
}
model.ModelEndUpdateL();
iEikonEnv->ReadResourceL(bb,R_STR_APP_TITLE);
ViewContext()->AddTextL(1,bb);
}
The emulator typically displays the following content:
Figure 2 Sockets1 example application
The screen shot shows that a protocol named TCP exists, it has a type of KSockStream (Type:1), it implements the KProtocolInetTcp protocol (Prot:6), and supports the KAfInet address family (Addr:2048). Similarly a protocol named udp exists. It is of type KSockDatagram, implements the KProtocolInetUdp protocol and supports the KAfInet address family.
RHostResolver
In simple terms, we obtain a connection to a remote endpoint by specifying the address of that endpoint. In practice, this is often harder to achieve as the address of the remote endpoint may not be easily known. In TCP/IP the address is often fixed; it is the IP address. For Bluetooth and infrared connections there is no permanent network and connections are typically created as and when they are required.
In practice, Bluetooth and infrared connections are preceded by a discovery phase, whereby one device searches for other devices within range that offer the required services. With TCP and UDP, servers are often known by a textual name which needs to be converted to an underlying IP address via the Domain Name Service (DNS). The RHostResolver object encapsulates the functionality required to determine the remote endpoint address.
Obtaining endpoint addresses can take time. Therefore, while both synchronous and asynchronous variants of the required functions exist, we recommend that you use the asynchronous variants. Doing so requires you to use active objects, as shown in the Sockets1 example application:
void CSocketEngine::Connect()
// Attempt to connect to the server defined by iServerName.
{
TInetAddr addr;
if (addr.Input(iServerName)==KErrNone)
Connect(addr.Address()); // Server name already a valid IP address.
else
{ // Need to look up IP addr using DNS.
TInt err=iHostResolver.Open(iSocketServer,KAfInet,KProtocolInetUdp);
if (err==KErrNone)
{
iHostResolver.GetByName(iServerName,iNameEntry,iStatus);
}
else
{ // Deal with all errors in the RunL().
TRequestStatus* q=(&iStatus);
User::RequestComplete(q,err);
}
iState=ESeLookingUp; // Now started request.
SetActive();
}
}
Our objective is to establish a socket based connection to the remote server. If the iServerName field already contains an IP address, such as 192.168.0.1, we do not need to resolve the name: it is already resolved! We can use the address immediately. Conversely if the iServerName field contains some text such as www.uiq.com, we need to convert the text into an IP address. We need to use DNS to translate the textual address into an IP address.
Our application requests the RHostResolver to convert the textual name into a resolved TNameEntry object asynchronously by calling the iHostResolver.GetByName() method. When the asynchronous request completes, our RunL() will be called.
void CSocketEngine::RunL()
{
switch (iState)
{
case ESeLookingUp: // Name look up has completed.
iHostResolver.Close();
iState=ESeNotConnected;
if (iStatus==KErrNone)
{
// DNS look up successful – attempt
// to connect to that IP address.
Connect(TInetAddr::Cast(iNameEntry().iAddr).Address());
}
else
{
// Error handling code.
}
break;
// Other cases, removed for clarity.
}
}
We extract the IP address from the TNameEntry object with the TInetAddr::Cast(iNameEntry().iAddr).Address() code, passing the resultant address to our socket connect method:
void CSocketEngine::Connect(const TUint32 aAddr)
{
TInt err=iSocket.Open(iSocketServer,KAfInet,KSockStream, KProtocolInetTcp);
if (err==KErrNone)
{
iAddress.SetPort(iPort);
iAddress.SetAddress(aAddr);
// Initiate socket connection (async)
iSocket.Connect(iAddress,iStatus);
}
else
{
// Deal with errors in RunL().
TRequestStatus* q=(&iStatus);
User::RequestComplete(q,err);
}
iState=ESeConnecting;
SetActive();
}
The socket connection request is asynchronous and so will call our RunL() when it completes:
void CSocketEngine::RunL()
{
switch (iState)
{
case ESeConnecting:
if (iStatus==KErrNone)
{ // Sucessfully connected..
iState=ESeConnected;
}
else
{ // Connect attempt failed.
iSocket.Close();
iState=ESeNotConnected;
}
break;
// Other cases, removed for clarity.
}
}
If we successfully connect, we can perform read and write operations as defined by the service we have connected to. In the case of our example application, we have chosen to connect to port 80 at www.uiq.com. This is the standard port for a server to deliver web pages via a higher level protocol called HTTP. We describe HTTP later.
This example application does not perform any RSocket read or write operations. For an example of how you may use those methods, you may wish to read the Bluetooth example application in the following section.
The screen shot below shows the log output when we run the Sockets1 example in the emulator and connect to to the www.uiq.com server:
Figure 3 Sockets1 log output
You should be able to see that we originally had the textual name www.uiq.com. This has been converted by the RHostResolver into the IP address 195.84.17.233.
CAPABILITIES
In order to successfully use the RHostResolver, our application needs NetworkServices capabilities. Without that capability you will get the rather unexpected error code -5120, no response from the DNS server when you call GetByName().
RConnection
As you can see in the Sockets1 example, there is no specific requirement to define which connection preferences or access point should be used to establish a physical connection. In the emulator, there is often only a single access point which gets used automatically.
As long as there is at least one access point defined, UIQ 3 will have a preferred access point. The user can change the preferred access point in Internet accounts settings.
Figure 4 Internet account settings
The user can also tell UIQ 3 to display the connection dialog for manual selection. This setting is also stored in the communications database CommsDat. Sony Ericsson phones allow the user to create groups of access points, where a group consists of a list of IAPs.
In our Sockets1 example we used the RHostResolver::Open(RSocketServ&, TUint, TUint) variant of the open method. If we wanted more control over which access point should be used we can use the RHostResolver::Open(RSocketServ&, TUint, TUint, RConnection&) variant.
Similarly our application uses the RSocket::Open(RSocketServ& ,TUint, TUint, TUint) overload of the open method. An alternative is to use the RSocket::Open(RSocketServ& ,TUint, TUint, TUint, RConnection&) variant.
A detailed explanation and examples of using RConnection and its interaction with CommsDat is beyond the scope of this book. The Connection Management section of the UIQ 3 SDK documentation has further information about using RConnection, including some usage guidelines.
Secure Sockets
Secure sockets enable applications to transfer encrypted content over a public network with both endpoints having the opportunity to authenticate each other.
In Symbian OS, secure socket support is provided by a set of plug-ins to the socket server. The default plug-ins support Transport Layer Security v1.0 (TLS1.0) and the Secure Socket Layer v3.0 (SSL3.0). A full specification of these protocols is contained within RFC 2246.
A secure socket implementation secures an already connected socket. In the Sockets1 example, once we successfully established a socket level connection, we could then transfer content. Prior to transferring content over a secure socket, a further step is required:
void CSecureSocketEngine::RunL()
// One of our requests has completed.
{
switch (iState)
{
case ESeConnecting:
if (iStatus==KErrNone)
{
TRAPD(err,
_LIT(KTLS1,"TLS1.0");
iSecureSocket = CSecureSocket::NewL(iSocket,KTLS1);
);
if (err!=KErrNone)
{
iSocket.Close();
iState=ESeNotConnected;
}
else
{ // Perform a client handshake with the
// server to establish security.
iSecureSocket->StartClientHandshake(iStatus);
iState=ESeClientHandshake;
SetActive();
}
}
else
{ // Connect attempt failed, allow
// retry or perhaps a different address.
iSocket.Close();
iState=ESeNotConnected;
}
break;
// Other cases removed for clarity.
}
}
We create a CSecureSocket object, passing the connected socket handle and an indication as to which protocol is to be used. Assuming that we successfully create the CSecureSocket object, we call the asynchronous StartClientHandshake() method.
A Security Information dialog is displayed in the emulator. Selecting Yes will enable the StartClientHandshake() functionality to continue.
Figure 5 SecureSockets example application
Eventually, the StartClientHandshake() function completes its task and our RunL() is called:
void CSecureSocketEngine::RunL()
// One of our requests has completed.
{
switch (iState)
{ // Establishing secure connection.
case ESeClientHandshake:
if (iStatus==KErrNone)
{
iSecureSocket->RecvOneOrMore(iRecvBuffer,iStatus,iXfrLen);
iState=ESeReading;
SetActive();
}
else
{ // Failed to get secure connection.
iSecureSocket->Close();
delete(iSecureSocket);
iSecureSocket=NULL;
iSocket.Close();
iState=ESeNotConnected;
}
break;
// Other statements removed for clarity.
}
}
If the request completes successfully, we can start reading and writing content over the secure socket. Rather than use the RSocket methods, you need to use the equivalent CSecureSocket methods as shown in the code fragment.
Code using secure sockets will work only on a real phone, so you will need to test this part of your code on a phone.
Bluetooth Technology
Generally, Bluetooth connections are established in an ad hoc fashion, for example, when a mobile phone and a Bluetooth headset come in to range.
As with most communications systems one device will act as a server, listening out for incoming connection requests, while the other will initiate the connection. In a Bluetooth connection either device can act as the server and the other act as the connection initiator. In our example application we let the user decide, via a menu option, which device should listen for incoming connection requests. In a commercial grade application it is possible for both devices to be set up to listen for connection requests. Whenever an action occurs on one device that requires a connection to be made, that device can change from listening for incoming connections to initiating the connection.
CAPABILITIES
Our example application requires the LocalServices capability. This is necessary to use both the CSdpAgent and RSocket classes.
Use of Active Objects
The Bluetooth example requires extensive use of active objects. Using active objects has several consequences:
- Our application must have an active scheduler in the thread performing the Bluetooth communications. A standard UIQ application primary thread always has a scheduler.
- Our application can process other events between the time a Bluetooth request is started and the time it completes.
A general design philosophy with active objects is that one active object represents one event source. The events that we need to manage are:
- asynchronous writing (transmission) of data
- asynchronous reading (reception) of data
- asynchronous connection establishment.
This suggests that we might need three active objects. If we look more closely, we discover that while both asynchronous read and write operations can be outstanding simultaneously, neither the read nor write can be outstanding at the same time as the connection request is outstanding – you cannot read or write if there is no remote end point to read from or write to! In our application we have chosen to merge the connection and read events and handle them with a single active object, demonstrating that this is reasonable. A potential disadvantage to this approach is that we need to run a slightly more complex state machine to track the current status.
Accepting Incoming Connection Requests
To accept an incoming Bluetooth connection request, the application needs to perform a number of tasks:
- determine a free port on which connections can be made
- decide the security policy for the application
- publish the availability and service type being offered by the application.
The class CBtConnectionListener is used to accept incoming connection requests. The ConstructL() method of this class is:
void CBtConnectionListener::ConstructL()
{ // Any write requests are handled by this active object.
iWriteChannel=new(ELeave)CBtWriter(iSocket,iObserver);
User::LeaveIfError(iListener.Open(iSocketServer,KRFCOMMDesC));
TInt port;
User::LeaveIfError(iListener.GetOpt(KRFCOMMGetAvailableServerChannel,KSolBtRFCOMM,port));
TBTSockAddr addr;
addr.SetPort(port);
// Setup the security settings for a point to point BT connection.
TBTServiceSecurity serviceSecurity;
serviceSecurity.SetUid(KUidServiceSDP);
serviceSecurity.SetAuthentication(EFalse);
serviceSecurity.SetEncryption(EFalse);
serviceSecurity.SetAuthorisation(ETrue);
serviceSecurity.SetDenied(EFalse);
addr.SetSecurity(serviceSecurity);
// Now request we start listening on the indicated
// port for connection requests.
User::LeaveIfError(iListener.Bind(addr));
User::LeaveIfError(iListener.Listen(1));
// Connect to the service discovery subsystem.
User::LeaveIfError(iSdp.Connect());
User::LeaveIfError(iSdpDatabase.Open(iSdp));
// We want a serial port service.
iSdpDatabase.CreateServiceRecordL(KSerialPortServiceClass,iRecordHandle);
// With the following protocol description
// this needs to match the lSerialPortAttributeList[]
// structure - since thats what were going
// to parse against.
CSdpAttrValueDES* sdpAttr= CSdpAttrValueDES::NewDESL(NULL);
CleanupStack::PushL(sdpAttr);
TBuf8<1> bb8;
bb8.Append(port);
sdpAttr
->StartListL()
->BuildDESL()
->StartListL()
->BuildUUIDL(KL2CAP)
->EndListL()
->BuildDESL()
->StartListL()
->BuildUUIDL(KRFCOMM)
->BuildUintL(bb8)
->EndListL()
->EndListL();
// And add that to the SDP database record.
iSdpDatabase.UpdateAttributeL(iRecordHandle,KSdpAttrIdProtocolDescriptorList,*sdpAttr);
CleanupStack::PopAndDestroy(sdpAttr);
// Add short human readable name for the service.
_LIT(KUiqBtExample,"UiqBtExample");
iSdpDatabase.UpdateAttributeL(iRecordHandle,
KSdpAttrIdBasePrimaryLanguage+KSdpAttrIdOffsetServiceName,
KUiqBtExample);
// Add human readable description for the service.
_LIT(KUIQBookExampleBlueToothservice,"UIQ, Book Example, BlueTooth service");
iSdpDatabase.UpdateAttributeL(iRecordHandle,
KSdpAttrIdBasePrimaryLanguage+KSdpAttrIdOffsetServiceDescription,
KUIQBookExampleBlueToothservice);
}
Firstly, we open a listening RSocket, specifying the RFCOMM protocol. We then request a free port on which we can listen. It is important to note that the port number can vary between requests, depending on the state of the rest of the phone. After defining the security policy that we wish our application to implement we bind the listening socket to the port and security policy, via a TBTSockAddr class. We then start listening for incoming connection requests by calling iListener.Listen(1). The parameter 1 specifies a queue of 1 slot for outstanding connection requests. This is sufficient for our example since we only support a single point to point connection.
For a remote device to successfully establish a socket based connection, it needs to know the port on which the server is listening. In our application this port is dynamically allocated, so a mechanism is required to communicate this information prior to the socket connection attempt.
The Bluetooth specification defines a Service Discovery Database which facilitates the discovery of services supplied by a device. A server typically publishes the services it supports within the Service Discovery Database. Remote devices read this database to determine if there is an appropriate service to connect to.
In our application we publish a record which includes the port information. The remote device locates this database record and extracts the port information, thereby discovering which port to connect to.
Access to the Service Discovery Protocol Database is via a Symbian OS process. We therefore have to connect to this process before we can open a handle to the actual database:
User::LeaveIfError(iSdp.Connect());
User::LeaveIfError(iSdpDatabase.Open(iSdp));
Once we have an open handle to the database, we can create a record within the database:
iSdpDatabase.CreateServiceRecordL(KSerialPortServiceClass,iRecordHandle);
The CreateServiceRecordL() returns a handle to the record within the iRecordHandle property. In our application, we create a record with service type attribute KSerialPortServiceClass. While a full description of Bluetooth service classes is beyond the scope of this book, in summary, a service class provides an indication of the capabilities of the service and, more particularly, defines the attributes which must appear along with the service class, such that the remote device can parse the service record and understand the content.
Service classes are identified with unique numbers. The Bluetooth specification pre-defines a set of service class and attribute values. In our application we have chosen to use the pre-defined Serial Port Service class. This in turn defines the attributes and values required within the record data.
The code below creates the record content containing the correct attributes and values for a serial port service database record:
CSdpAttrValueDES* sdpAttr=CSdpAttrValueDES::NewDESL(NULL);
CleanupStack::PushL(sdpAttr);
TBuf8<1> bb8;
bb8.Append(port); // The port to connect to.
sdpAttr
->StartListL()
->BuildDESL()
->StartListL()
->BuildUUIDL(KL2CAP)
->EndListL()
->BuildDESL()
->StartListL()
->BuildUUIDL(KRFCOMM)
->BuildUintL(bb8)
->EndListL()
->EndListL();
// And add that to the SDP database record.
iSdpDatabase.UpdateAttributeL(iRecordHandle,
KSdpAttrIdProtocolDescriptorList,
*sdpAttr);
CleanupStack::PopAndDestroy(sdpAttr);
Security Settings
Your application decides the security settings. We have chosen to SetAuthorisation(ETrue) meaning that a user will be prompted to accept the connection attempts, however we have SetAuthentication(EFalse) meaning that no pin number exchange is required. Your choice of security settings should reflect the importance of the information being transferred. Since our application only transfers text strings and displays them on screen, this low level of security is appropriate. However, if your application transfers information that would otherwise remain private to a user, such as contact details or address book entries, the level of security should be higher, almost certainly requiring authentication and possibly requiring encryption.
Listening for Incoming Connections
Once our service has been published, we can start listening for incoming socket level connection requests:
void CBtConnectionListener::StartListening()
// Start the acceptance of a connection from
// a remote Bluetooth device.
{
iSocket.Open(iSocketServer);
SetAvailability(0xFF);
iListener.Accept(iSocket,iStatus);
SetActive();
iState=EListenerStateConnecting;
}
Firstly, we open a blank RSocket into which any incoming connection request will be transferred. If you recall, we have a listening queue size of 1. The first pending connection from that queue will be assigned to this newly opened blank socket. After successful completion of the Accept() request:
- The
iListenersocket can be used to make further connections by issuing furtherAccept()requests on alternate blank sockets. Our application only supports a single connection at a time so we have not implemented such functionality. - The
iSocketobject represents a connection between devices and can be used to transfer content using the read and write methods.
In our state machine we set iState to EListenerStateConnecting. This enables us to know how to handle any cancel requests or events completions. When the Accept() request completes, our RunL() method will be called:
void CBtConnectionListener::RunL()
// A socket event has occured.
{
switch (iState)
{
case EListenerStateConnecting:
SetAvailability(0x00); // No more connections at this time.
if (iStatus.Int()==KErrNone)
{
iState=EListenerStateConnected;
Read();
iObserver->StatusInfo(EBtsConnectionEstablished,KErrNone);
}
else
{ // Shutdown and start again.
_LIT(KListenError,"Listen Error");
iObserver->LogInfo(KListenError);
iState=EListenerStateIdle;
iSocket.Close();
StartListening(); // Re-try connection.
}
break;
// Other events removed for clarity.
}
}
If we connected successfully, we then issue a read request to accept any data the remote device may wish to transfer. On failure we close down the blank socket and retry for a connection.
Processing Incoming Data
The format and content of data transferred between devices is specific to the applications involved. A socket, whether it is a Bluetooth, infrared or TCP type socket, simply transfers a stream of bytes between endpoints. Our application transfers discrete packets where each packet contains one unit of information: a textual message. In general, the receiving phone needs to perform some processing of the data to produce the desired output. Our application is required to display one line of text for each message. It therefore needs to process any received data into the individual text messages for display.
Some applications may choose not to transfer data within discrete packets. They simply send a byte stream containing commands and content. The commands and content may span one or more buffers of data. Conversely a buffer may contain more than one command and associated content. For example, an application that transfers photographs from one device to another is unlikely to send the entire photograph in a single buffer in a RAM limited environment such as a mobile phone.
The content being transferred dictates how your application chooses to implement the read and write operations. Our example application has been defined to transfer text messages where no text message will be longer than 128 bytes. We could read whatever data is available from the socket using the RecvOneOrMore() method and then assemble it back into text messages, before passing it to the display code. Instead, we adopt a slightly different approach.
Given the constraints of our application, we can send a small piece of information at the start of every packet of data to be transferred. By doing this, we can easily perform a quick validation check and know how much more data to read from the socket in order to obtain the remainder of the packet. In effect, we use the socket to assemble all the data belonging to the packet before it is delivered to our application.
The read code comprises two methods:
void CBtConnection::Read()
// Start the read of a packet header.
{
iSocket.Recv(iFrameHdr,0,iStatus);
SetActive();
iReadingState=EReadingHeader;
}
void CBtConnection::ReadContent(void)
// Start a read of data content.
{
iDataPtr.Set((TUint8*)iData.Ptr(),0,iFrameHdr[5]);
iSocket.Recv(iDataPtr,0,iStatus);
SetActive();
iReadingState=EReadingContent;
}
The Read() method requests exactly six bytes of information, that is the size of the iFrameHdr descriptor. Once the header arrives, our RunL() method is called:
void CBtConnectionListener::RunL()
// A socket event has occured.
{
switch (iState)
{
case EListenerStateConnected:
// Data arrived for us to process.
if (iReadingState==EReadingHeader)
{
TInt error=iStatus.Int();
if (error==KErrNone)
{ // We have got the header.
TPtr8 q((TUint8*)iFrameHdr.Ptr(),5,5);
if (q==KBluetoothHeaderText)
{
ReadContent(); // Get the frame data.
break;
}
// Header is broken shut down and re-start.
error=KErrCorrupt;
}
iObserver->StatusInfo(EBtsDisconnected,error);
iState=EListenerStateIdle;
iSocket.Close();
StartListening(); // Re-try connection.
}
else
// .....removed for clarity.
break;
// Other cases removed for clarity.
}
}
If we successfully receive the header and it passes the validation check, we can use the length information transferred as part of this header to read the rest of the packet by calling the ReadContent() method.
Efficiencies and Complexities of this Approach
As with most software tasks, there are trade-offs between various approaches. Using the above approach to read content means that for every packet transferred, we perform exactly two Read() requests: we are delivered two events and thus execute RunL() twice. Had we chosen to structure our application to use RSocket.RecvOneOrMore(), the chances are that in the vast majority of cases only a single read would be required to deliver the whole packet, however this is not guaranteed. We would therefore need some logic to process the incoming data into text messages, caching partial messages, before they can be delivered to the UI component. Both approaches have advantages and disadvantages, and you should consider which is best for your application.
| Pro tip:
There is nothing of particular significance in the size or content of the header information that we have chosen to transfer at the start of every packet. We are simply using the five characters defined by |
Creating a Connection
To create a connection with a remote device we need to:
- discover what devices are currently available within the local vicinity
- connect to and read the selected device Service Discovery Database
- read and process any records associated with the service class our application has been defined to connect to
- obtain the port number that the remote device is listening for socket level connection requests on
- request a socket level connection.
Discovering Devices
The following code is used to discover what devices are available within the local vicinity:
void CBtConnectionCreator::StartDiscoveryL()
{
iRemotePort=(-1);
CBTDeviceArray* btDeviceArray = new(ELeave)CBTDeviceArray(2);
CleanupStack::PushL(btDeviceArray);
TRAPD(err,
CQBTUISelectDialog* dlg = CQBTUISelectDialog::NewL(btDeviceArray);
if (dlg->RunDlgLD(KQBTUISelectDlgFlagNone))
{
if (btDeviceArray->Count()>0)
{
CBTDevice* dev=btDeviceArray->At(0);
iRemoteDevAddr=dev->BDAddr();
iRemoteDevClass=dev->DeviceClass();
TBTDeviceName8 name(dev->DeviceName());
iRemoteDevName = BTDeviceNameConverter::ToUnicodeL(name);
iSdpAgent=CSdpAgent::NewL(*this,iRemoteDevAddr);
iSdpSearchPattern=CSdpSearchPattern::NewL();
iSdpSearchPattern->AddL(KSerialPortServiceClass);
iSdpAgent->SetRecordFilterL(*iSdpSearchPattern);
// Calls back NextRecordRequestComplete().
iSdpAgent->NextRecordRequestL();
iState=ECreatorStateFindService;
iStatus=KRequestPending;
SetActive();
}
}
);
if (err!=KErrNone)
{
delete(iSdpAgent);
iSdpAgent=NULL;
delete(iSdpSearchPattern);
iSdpSearchPattern=NULL;
iObserver->StatusInfo(EBtsConnectionEstablished,err);
iState=ECreatorStateIdle;
}
CleanupStack::PopAndDestroy(btDeviceArray);
User::LeaveIfError(err);
}
We launch the standard UIQ 3 Bluetooth device selection dialog using CQBTUISelectDialog. This performs the local search and presents a list of devices for the user to choose between. Once a device has been chosen we can use the low level Bluetooth device address to connect to the Service Discovery Database on the remote device. Since our connection listener is defined as providing a KSerialPortServiceClass we should filter the database records to only contain those associated with this service class.
Searching for Services
As shown in the previous code fragment, the search for and delivery of database records is asynchronous. Rather than present any direct active object interface, the CSdpAgent performs its processing behind the scenes, only calling us back when required. From an application level perspective, we have a single active event running. Our application chooses to represent this by setting up a pseudo-event source and recording the current state with the following code:
iState=ECreatorStateFindService; iStatus=KRequestPending; SetActive();
In this way, cancelling the active search can be managed and it does not matter which of the various callbacks fail or succeed. We can handle the completion of the search in a single place, our RunL().
The CSdpAgent requires that we implement the MSdpAgentNotifier interface. As each individual record from the remote Service Discovery Database is retrieved so the NextRecordRequestComplete() method is called:
void CBtConnectionCreator::NextRecordRequestComplete(
TInt aError,
TSdpServRecordHandle aHandle,
TInt aTotalRecordsCount)
{
if (aError==KErrEof) // Scanned all records – see if found
// the data were looking for.
{
TBuf<32>bb;
_LIT(KErrEof,"KErrEof %d");
bb.Format(KErrEof,iRemotePort);
iObserver->LogInfo(bb);
RequestComplete(iRemotePort==(-1) ? KErrNotFound : KErrNone);
}
else if (aTotalRecordsCount==0)
RequestComplete(KErrNotFound);
else if (aError!=KErrNone)
RequestComplete(aError);
else
{
// We want attributes of the record.
TRAPD(err,
iSdpAgent->AttributeRequestL(aHandle,
KSdpAttrIdProtocolDescriptorList));
if (err!=KErrNone)
RequestComplete(err);
}
}
If there are no more records, aError will be set to KErrEof. By this time we anticipate that we will have located the KSerialPortServiceClass record describing the connection on which the remote copy of our application is listening. Our application validates that we have obtained a remote port value. If so, the search has been successful.
When we are delivered a record, we need to fetch the attributes associated with the record. As a reminder, the attributes contain the port information we are searching for. Once fetched, the AttributeRequestResult() method is called back:
void CBtConnectionCreator::AttributeRequestResult(
TSdpServRecordHandle aHandle,
TSdpAttributeID aAttrID,
CSdpAttrValue* aAttrValue)
{
// Scan that set of attribute/values from the start.
iNodeIndex=0;
TRAPD(err,aAttrValue->AcceptVisitorL(*this));
if (err!=KErrNone)
RequestComplete(err); // Cant run AcceptVisitorL().
else if (lSerialPortAttributeList[iNodeIndex].iAction==EAttrItemFinished)
{
iRemotePort=iExtractedValue;
// Reached end of struct and all as expected –
// so we have located the port.
TBuf<32>bb;
_LIT(KPortIndex,"Port %d, Index %d");
bb.Format(KPortIndex,iRemotePort,iNodeIndex);
iObserver->LogInfo(bb);
}
}
To parse the attributes we call the AcceptVistorL() method. We are expected to implement the MSdpAttributeValueVisitor interface to handle callbacks generated during the parsing. In summary, for each node in the attributes, the VisitAttributeValueL() method is called back.
void CBtConnectionCreator::VisitAttributeValueL(
CSdpAttrValue &aValue,
TSdpElementType aType)
{
TBuf<32>bb;
bb.Format(_L("VisitAttributeValueL %d"),iNodeIndex);
iObserver->LogInfo(bb);
switch (lSerialPortAttributeList[iNodeIndex].iAction)
{
case EAttrItemCheckType:
if (lSerialPortAttributeList[iNodeIndex].iElementType!=aType)
User::Leave(KErrGeneral);
break;
case EAttrItemCheckTypeAndValue:
// Check type and values are as expected.
if (lSerialPortAttributeList[iNodeIndex].iElementType!=aType)
User::Leave(KErrGeneral);
// Check value is as expected.
switch (aValue.Type())
{
case ETypeUint:
if (aValue.Uint() != lSerialPortAttributeList[iNodeIndex].iValue)
User::Leave(KErrArgument);
break;
case ETypeInt:
if (aValue.Int()!=lSerialPortAttributeList[iNodeIndex].iValue)
User::Leave(KErrArgument);
break;
case ETypeBoolean:
if (aValue.Bool()!=lSerialPortAttributeList[iNodeIndex].iValue)
User::Leave(KErrArgument);
break;
case ETypeUUID:
if (aValue.UUID()!=TUUID(lSerialPortAttributeList[iNodeIndex].iValue))
User::Leave(KErrArgument);
break;
default:
break;
}
break;
case EAttrItemCheckEnd:
case EAttrItemFinished: // Too many items in list.
User::Leave(KErrGeneral);
break;
case EAttrItemReadValue:
// Check type is as expected and read remote value.
if (lSerialPortAttributeList[iNodeIndex].iElementType!=aType)
User::Leave(KErrGeneral);
// Possibly found the remote port info.
iExtractedValue=aValue.Uint();
_LIT(KEAttrItemReadValue,"EAttrItemReadValue: %d");
bb.Format(KEAttrItemReadValue,iExtractedValue);
iObserver->LogInfo(bb);
break;
default:
break;
}
NextNodeL();
}
Since we are expecting a specific structure, the one defined by the connection listener, we can run a small state machine to validate each of the nodes delivered. The more validation that we perform, the more likely it is that our application will correctly identify the required record.
If all goes well, and the attribute structure matches the one we are expecting, we will have extracted the remote port published by the connection listener. Upon completion of the search, we signal our pseudo-event source. This will eventually call our RunL():
void CBtConnectionCreator::RunL()
{
switch (iState)
{
case ECreatorStateFindService:
delete(iSdpAgent);
iSdpAgent=NULL;
delete(iSdpSearchPattern);
iSdpSearchPattern=NULL;
if (iStatus.Int()==KErrNone)
{ // Sucessfully discovered the remote port.
Connect();
}
else
{ // Report we failed to connect.
iObserver->StatusInfo(EBtsConnectionEstablished,iStatus.Int());
iState=ECreatorStateIdle;
}
break;
// Removed for clarity....
}
}
If the search completed successfully, we can proceed to create a socket connection:
void CBtConnectionCreator::Connect()
// Issue the connect request now we have
// the remote device address and port.
{
iSocket.Open(iSocketServer,KRFCOMMDesC);
TBTServiceSecurity serviceSecurity;
serviceSecurity.SetAuthentication(EFalse);
serviceSecurity.SetEncryption(EFalse);
serviceSecurity.SetAuthorisation(ETrue);
serviceSecurity.SetDenied(EFalse);
TBTSockAddr addr;
addr.SetBTAddr(iRemoteDevAddr);
addr.SetPort(iRemotePort); // As fetched from remote.
addr.SetSecurity(serviceSecurity);
iSocket.Connect(addr,iStatus);
SetActive();
iState=ECreatorStateConnecting;
}
As with the connection listener, we open an RFCOMM socket and set up the required security. Since we are establishing a Bluetooth connection, we need to specify the Bluetooth device address, obtained from the characteristics of the device chosen by the user, and the port as obtained from the Service Discovery Database search.
Once the connection attempt is successful, we can issue a Read() request awaiting data from the remote device.
Reading and Writing with Sockets
Sockets are simply the endpoints of a logical connection. Whatever your application writes to a socket will be available to read at the other end. A socket, whether it be a Bluetooth, infrared or TCP type socket, does not place any restrictions on or interpret content.
Sockets in Symbian OS, in common with other systems, expect to be presented with a stream of bytes to transfer. That stream of bytes is delivered to the remote end. Symbian OS uses Unicode to represent text. In particular, it uses the default UTF-16 encoding scheme which uses two bytes to represent a single character. Due to this mismatch in element size, we need to convert the text to a stream of bytes.
For simplicity, our application has assumed that any text that needs to be transferred only contains characters within the ASCII character set. By making this assumption we can convert between 8 and 16-bit descriptors simply by using the Copy() methods.
This approach is unlikely to be acceptable in a commercial quality application since information loss is likely. One solution is to send each character as two bytes. In such a scheme, both ends need to agree in which order the low and high bytes are transferred. One disadvantage of this approach is the amount of redundancy in the data if the majority of characters belong to the ASCII character set.
An alternative approach is to use the UTF-8 encoding scheme. In such a solution, the UTF-16 encoded text is converted to UTF-8. The UTF-8 data is now in a form suitable for direct transfer via sockets. The receiving end would convert the UTF-8 encoded text back into UTF-16 resulting in lossless transfer of Unicode text between phones. Symbian OS contains support for translating between character sets in the form of the CnvUtfConverter class. This class is documented in the UIQ 3 SDK so we will not discuss it further here.
HyperText Transfer Protocol
The HyperText Transfer protocol (HTTP) is an industry-standard communications protocol for the transfer of content between devices. The full protocol specification is defined in RFC 2616 and is available at www.ietf.org.
HTTP is said to be an application level protocol. It places few restrictions on the mechanism by which content is transferred between devices other than assuming the transport is reliable. In practice the TCP/IP protocol is normally used to transfer HTTP data between devices.
HTTP is usually associated with transferring web pages between a server and a browser, however that is only one of many possible uses of the protocol. Applications are free to define their own uses.
The HTTP specification defines a number of high level commands which can be transferred between devices. The two most common are called GET and POST. A GET command is usually sent by a client device when it wants to retrieve information from the remote device. The POST request is typically used to send data to a remote device. Since both commands can send information with the request and accept data as part of the command response it is possible to send data with a GET request and retrieve information with a POST.
Universal Resource Identifier
In order to fetch or store content, it needs to be identified. A text string called a Universal Resource Identifier or URI is used to identify items. Strictly speaking a URI is different to a Universal Resource Location or URL, however many servers provide defaults so the subtle differences between the two terms have become eroded. Today the term URL is frequently used to cover URI, Universal Resource Name (URN) and URLs.
In general, a URL comprises a number of constituent parts including:
- the scheme, such as HTTP, FTP, FILE, MAILTO
- a remote computer name
- a port on the remote computer
- a path on the remote computer
- a query string
- fragment information.
The rules describing which components are optional, which are mandatory and what default values are supplied are complex and beyond the scope of this book. Suffice to say a correctly formed URL is required to successfully transfer content between devices.
HTTP Headers
The HTTP protocol is designed to be a flexible and extendable application level protocol. In such systems, care is required to ensure both end points understand the limitations of the other, particularly if general purpose applications such as web servers and browsers are communicating with each other. For example, if a server can deliver images, the client needs to understand in which format they are delivered and, probably, restrict the server to only supply formats that the client can process. The mechanism by which the devices can communicate preferences, limitations, status and other control information is through the use of headers.
HTTP Transactions
To actually perform a GET or POST type request you need to:
- specify a URL
- define the set of headers
- define which operation you are performing (GET or POST)
- supply any data associated with the operation
- perform the operation
- receive a response from the remote device.
This set of discrete actions is combined into a single transaction. An application is said to be performing a GET or POST transaction.
String Pools
Despite only ever being processed by computers, many communications protocols are defined using English language structure and text. For example, the HTTP protocol sends the textual characters ‘G’, ‘E’, ‘T’ to inform the remote computer a GET command has been issued, similarly the characters ‘P’, ‘O’, ‘S’, ‘T’ are used to indicate a POST command. A remote computer has to convert between this variable length human readable text (often called tokens) and an internal action. Since such tokens are static and do not vary between spoken languages Symbian OS facilitates a compile time generation of sets of tokens that can be processed efficiently. In Symbian OS these are called string pools. The HTTP protocol contains a significant number of these tokens. Rather than individual applications having to manage a string pool they can use the HTTP-specific string pool supplied as part of Symbian OS.
Capabilities
Our example application uses the default transport, TCP, to transfer HTTP data between phones. The HTTP framework uses the sockets interface to supply the TCP functionality. In order to open a TCP style socket an application is required to have NetworkServices capability. Therefore our application needs the NetworkServices capability to perform HTTP transfers.
HTTP and Mobile Phones
You may recall the earlier discussion around mobile networks and, in particular, Network Address Translation (NAT). HTTP works well in the mobile network environment since the mobile phone will initiate the data transfer regardless of whether it is sending or receiving information. It will always send the uplink packet required by NAT to form the address translation table entry.
HTTP GET
Bringing all this together to perform a simple HTTP GET request results in the following code taken from the Http1 example application:
void CHttpEngine::SetHeaderL(
RHTTPHeaders aHeaders,
TInt aHdrField,
const TDesC8& aHdrValue)
{
RStringF str=iSession.StringPool().OpenFStringL(aHdrValue);
CleanupClosePushL(str);
THTTPHdrVal val(str);
aHeaders.SetFieldL(iSession.StringPool().StringF(
aHdrField,RHTTPSession::GetTable()),val);
CleanupStack::PopAndDestroy(&str);
}
void CHttpEngine::GetContentL(const TDesC8& aUrl)
{
TUriParser8 uri;
User::LeaveIfError(uri.Parse(aUrl));
RStringF method = iSession.StringPool().StringF(
HTTP::EGET,RHTTPSession::GetTable());
iTransaction = iSession.OpenTransactionL(uri,*this,method);
RHTTPHeaders hdrs=iTransaction.Request().GetHeaderCollection();
_LIT8(KUserAgent,"UIQ Http Example (1.0)");
SetHeaderL(hdrs,HTTP::EUserAgent,KUserAgent);
_LIT8(KAccept,"text/*");
SetHeaderL(hdrs,HTTP::EAccept,KAccept);
iTransaction.SubmitL();
}
As you can see, we generate a fully parsed URI in the uri object. We also obtain the string token identified by the HTTP::EGET enumeration from the HTTP specific string pool. We use these objects to create the transaction within which the HTTP GET request is performed. The HTTP infrastructure will automatically add those headers defined by RFC 2616 as being mandatory for an HTTP 1.1 client, so the application does not need to add these headers itself. In general, applications only need to focus on the headers associated with acceptable content types. In our example application, we have specified that we can only accept text content. We have not, however, defined for example what character set is acceptable.
Explaining the use of all HTTP headers is beyond the scope of this book, however the example application shows how you can add application specific headers to a transaction.
The HTTP transaction processing classes use asynchronous methods to perform the actual processing: rather than use threads, they use active objects.
This has several consequences:
- Our application must have an active scheduler in the thread performing the HTTP transaction. A standard UIQ application primary thread always has a scheduler.
- Our application can process other events between the time an HTTP transaction is started and the time it completes.
HTTP transactions requires us to implement the MHTTPTransactionCallback interface since that is the mechanism used to deliver events and content from the HTTP framework to individual applications:
void CHttpEngine::MHFRunL(RHTTPTransaction aTransaction,const THTTPEvent& aEvent)
// Note: this is not allowed to leave on some
// events... despite it being an L function.
{
switch (aEvent.iStatus)
{
case THTTPEvent::EGotResponseHeaders:
{ // HTTP response headers have been received.
_LIT(KEGotResponseHeaders,"EGotResponseHeaders");
iObserver->LogInfo(KEGotResponseHeaders);
RHTTPResponse resp=aTransaction.Response();
iStatusCode=resp.StatusCode();
iStatusText.Copy(resp.StatusText().DesC());
break;
}
case THTTPEvent::EGotResponseBodyData:
{// Some (more) body data has been received.
_LIT(KEGotResponseBodyData,"EGotRespBodyData");
iObserver->LogInfo(KEGotResponseBodyData);
MHTTPDataSupplier* body=aTransaction.Response().Body();
TPtrC8 bodyData;
body->GetNextDataPart(bodyData);
iObserver->ReportBody(bodyData);
body->ReleaseData();
break;
}
case THTTPEvent::EResponseComplete:
// Transaction complete (got all content etc).
_LIT(KEResponseComplete,"EResponseComplete");
iObserver->LogInfo(KEResponseComplete);
break;
case THTTPEvent::ESucceeded:
// Transaction finished - all ok.
aTransaction.Close();
iObserver->ReportEvent(KErrNone,iStatusCode,iStatusText);
iIsBusy=EFalse;
break;
case THTTPEvent::EFailed:
// Transaction finished – something fell over.
aTransaction.Close();
iObserver->ReportEvent(KErrGeneral,iStatusCode,iStatusText);
iIsBusy=EFalse;
break;
case THTTPEvent::ERedirectedPermanently:
_LIT(KERedirectedPermanently,"ERedirectedPermanently");
iObserver->LogInfo(KERedirectedPermanently);
break;
case THTTPEvent::ERedirectedTemporarily:
_LIT(KERedirectedTemporarily,"ERedirectedTemporarily");
iObserver->LogInfo(KERedirectedTemporarily);
break;
default:
{ // -46 means no NetworkServices CAPABILITY !
TBuf<64>bb;
_LIT(KUnhandled,"Unhandled: %d");
bb.Format(KUnhandled,aEvent.iStatus);
iObserver->LogInfo(bb);
break;
}
}
}
TInt CHttpEngine::MHFRunError(
TInt aError,
RHTTPTransaction aTransaction,
const THTTPEvent& aEvent)
// Called by internal active object if error occurs.
{
TBuf<64>bb;
_LIT(KMHFRunError,"MHFRunError: %d,%d");
bb.Format(KMHFRunError,aError,aEvent.iStatus);
iObserver->LogInfo(bb);
return(KErrNone);
}
In our application we have chosen to capture and display events as and when they occur. We place the latest event at the top of the list. The following screen shot is the result of performing an HTTP GET on the URL www.uiq.com.
Figure 6 Http1 example application
The screen shot shows that we received several EGotResponseBodyData events. We happen to receive multiple EGotResponseBodyData events containing 1406 characters followed by a single EGotResponseBodyData containing 626 characters. Finally, we received an EGotResponseBodyData event with zero length data.
Once all the EGotResponseBodyData events have been delivered, an EResponseComplete followed by an ESucceeded event are delivered by the framework. Our application has extracted the HTTP status code, 200, and status text message, OK from the transaction response headers. If you are familiar with the HTTP specification, you will recognize the code and text values also indicate that the GET transaction has completed successfully.
If your MHFRunL() method leaves for any reason, the HTTP framework will call the MHFRunError() method to enable your application to handle the error condition. We simply choose to add a new log message.
Our application has only implemented a sample of the full range of possible events defined by the THTTPEvent class. A commercial grade application needs to understand how each of the events can occur and deal with them appropriately. The full list of events can be found in the thttpevent.h file.
HTTP POST
To perform any truly meaningful tasks with HTTP POST you need to have a remote computer that can to understand the body sent as part of the post. In general, this means you need to have some kind of system dedicated to the task in hand. Since this book is generic in nature, our example application does not perform any specific task using HTTP POST. Rather, it uses the fact that most web servers will at least give you a response if you send them a POST request. This enables us to demonstrate how you might construct an application that uses HTTP POST requests without having to supply a specific HTTP server.
We have chosen to separate out the functionality associated with HTTP GET and POST into two separate engines. This is done to aid in the understanding of what is required to perform these tasks. It is possible to combine the two engines should your specific application need to perform HTTP GET and POST transactions.
As with an HTTP GET, the HTTP POST needs to bring together the concepts of URIs, headers and transactions as demonstrated by the following code:
void CHttpPostEngine::PostContentL(
const TDesC8& aUrl,
HBufC8* aData)
{
iData=aData;
TUriParser8 uri;
User::LeaveIfError(uri.Parse(aUrl));
RStringF method=iSession.StringPool().StringF(
HTTP::EPOST, RHTTPSession::GetTable());
iTransaction = iSession.OpenTransactionL(uri,*this,method);
RHTTPHeaders hdrs=iTransaction.Request().GetHeaderCollection();
_LIT8(KUserAgent,"UIQ Http Example (1.0)");
SetHeaderL(hdrs,HTTP::EUserAgent,KUserAgent);
_LIT8(KContentType,"text/plain");
SetHeaderL(hdrs,HTTP::EContentType,KContentType);
iTransaction.Request().SetBody(*this);
iTransaction.SubmitL();
}
The main differences between a GET and POST transaction are:
- the method name supplied to the transaction
- the headers used
- the fact we have some content to transfer.
Supplying Content to the POST Transaction
We inform the POST transaction that we have some content to transfer using the iTransaction.Request().SetBody(*this) method, which requires us to implement the MHTTPDataSupplier interface. Some applications may have large quantities of data to transfer and prefer to supply this data over several blocks. In our application we have chosen to supply all the content to transfer in a single block.
The OverallDataSize() should report the total amount of data that needs to be transferred; the GetNextDataPart() supplies one data block at a time. When all blocks have been supplied, the GetNextDataPart() should return ETrue. Until that time it should report EFalse, indicating further content is available.
The implementation of the MHTTPDataSupplier interface in our application is:
TBool CHttpPostEngine::GetNextDataPart(
TPtrC8& aDataPart)
{
aDataPart.Set(*iData);
return(ETrue);
}
void CHttpPostEngine::ReleaseData(void)
{
delete(iData);
iData=NULL;
}
TInt CHttpPostEngine::OverallDataSize(void)
{
if (!iData)
return(0);
return((*iData).Length());
}
TInt CHttpPostEngine::Reset(void)
{
return(KErrNotSupported);
}
To monitor and handle events generated by the HTTP framework, we also need to implement the MHTTPTransactionCallback interface. Apart from a few points of detail, the implementation for a POST transaction is the same as described previously for the GET transaction.
The result of performing an HTTP POST request on the URL www.uiq.com simply to demonstrate the code functioning will generate a log along the lines of:
Figure 7 Http1 application, HTTP POST log
Note that since we sent the POST request to www.uiq.com, the application will get response headers and response body since that is what a typical web server will do.
Messaging Architecture
Symbian OS contains an extensive system component called the messaging architecture or message store. This component facilitates the creation, sending, receiving and storing of messages, irrespective of which mechanism is used to transfer messages.
Figure 8 Symbian OS messaging architecture
Message Server
Symbian OS support for messaging is based around a client-server process model. The server manages the actual messages through a set of plug-ins, often called Message Type Modules or MTMs. Client access to the message server is through the CMsvSession object which encapsulates the IPC connection with the server.
A primary task of the message server is to provide thread safe shared access to all entries within the overall message store.
Message Entries
The message server manages a set of entries. An individual entry can be one of the following types:
- folder
- message
- attachment
- service.
Figure 9 Messaging tree structure
Entries may have child entries and are therefore managed within a tree like structure similar to files and folders within a file system. All entries are referenced by unique IDs, encapsulated by the TMsvId object.
Figure 10 Message entry structure
The message server pre-defines a number of TMsvId IDs (entries), including the following:
-
KMsvLocalServiceIndexEntryId -
KMsvGlobalInBoxIndexEntryId -
KMsvGlobalOutBoxIndexEntryId -
KMsvDraftEntryId -
KMsvSentEntryId -
KMsvDeletedEntryFolderEntryId.
At the root of the tree is the KMsvRootIndexEntryId, which leads to the KMsvLocalServiceIndexEntryId entry. At a minimum this contains the Inbox, Outbox, Drafts and Sent items folders. You can see this in Figure 9.
The Symbian OS messaging architecture is abstract in design, allowing many functions to be performed irrespective of entry type. These include navigating between parent and child entries, moving or copying entries between folders and deleting entries.
This abstract design facilitates the concepts of global Inbox and global Outbox. All entries, irrespective of specific entry details can be stored within the same folder. For example, you may have observed SMS messages together with Bluetooth or infrared messages stored in your phone’s Inbox.
The SMS MTM will store incoming messages in the global inbox. This allows you to use the synchronous methods in CMsvEntry such as ChangeL() and CreateL(). For POP/IMAP/SMTP MTMs, however, their incoming messages may be stored under the service entry (or potentially in a sub-folder in the service entry for IMAP subscribed folders). Because they are under the service entry, you have to use the asynchronous methods of CMsvEntry, as the MTM's server MTM implementation will have to deal with the DeleteL(), ChangeL() etc requests. However, all outgoing messages are generally stored in the Drafts/Outbox/Sent folders regardless of the MTM.
Message Entry Storage
In general, the content of a single message entry is distributed over three storage locations:
- a message server index entry represented by a
TMsvEntry - some persistent file storage represented by a
CMsvStore - a folder within the file system.
TMsvEntry
All entries, regardless of type, have an associated TMsvEntry. From an application programming perspective, each folder within the message store comprises an array or index of TMsvEntrys
If you look at the properties of a TMsvEntry (from msvstd.h) it shows a number of useful fields:
class TMsvEntry
{
private:
TMsvId iId;
TMsvId iParentId;
TInt32 iData;
TInt32 iPcSyncCount;
TInt32 iReserved; // Reserved for future proofing.
public:
TMsvId iServiceId;
TMsvId iRelatedId;
TUid iType;
TUid iMtm;
TTime iDate;
TInt32 iSize;
TInt32 iError;
TInt32 iBioType;
TInt32 iMtmData1;
TInt32 iMtmData2;
TInt32 iMtmData3;
TPtrC iDescription;
TPtrC iDetails;
};
Not surprisingly, the iId field contains the unique ID for a specific TMsvEntry. The iParentId contains the unique ID for the parent allowing us to traverse the tree hierarchy.
The iType field can take one of the following values allowing applications to distinguish between different types of entries:
-
KUidMsvRootEntry -
KUidMsvServiceEntry -
KUidMsvFolderEntry -
KUidMsvMessageEntry -
KUidMsvAttachmentEntry
The iMtm field allows applications to determine which MTM the entry is associated with. This is a key piece of information that enables applications to process entries fully.
A TMsvEntry also contains some generic fields such as iDate, iSize, iDescription and iDetails. These are sufficient for applications to display summary information, for example, in a list view of a particular folder, without having to fetch any other data associated with an entry. iDetails typically stores the address, while iDescription usually stores the Subject for an Email. The contents of these fields are MTM-specific.
CMsvStore
While some information is stored within the TMsvEntry the remainder is saved within a file store entry. The format depends on what type of entry is being represented and is typically used to store headers and body text should such items be present in a particular message type.
File system folder
All entries are associated with a specific folder within the file system where they may choose to store further information. As with the CMsvStore, the usage of this type of storage is optional and is MTM-specific. A typical usage may be to store the actual attachment files associated with attachment entries.
CMsvEntry
A CMsvEntry encapsulates the functionality to obtain all the component parts of an individual entry. In particular, a CMsvEntry does not represent a specific entry, only the ability to access the component parts of an entry.
A CMsvEntry can often be considered as an Iterator. Each folder effectively comprises a set of TMsvIds. A CMsvEntry can be set to each of these IDs in turn to be able to access information about each entry.
| Pro tip:
Since a |
Messaging Example Application
Our messaging application brings together many of the above objects to demonstrate how you may go about scanning the Inbox looking for a particular set of entries, and how you may go about creating and sending an entry. We will use SMS as an example because it is a common requirement and relatively simple to implement.
Our example application requires the NetworkServices, ReadUserData and WriteUserData capabilities to be able to read the user Inbox and transmit SMS messages.
- A first pass at scanning the Inbox to list all SMS entries might result in the following code:
void CMessageEngine::ConstructL()
{
// Get IPC connection to the meesage server process.
iMessageServer=CMsvSession::OpenSyncL(*this);
// We need a client side CClientMtmRegistry
// to obtain MTM objects.
iMtmRegistry = CClientMtmRegistry::NewL(*iMessageServer);
// Required to send SMS entries.
iSendSelection=new(ELeave)CMsvEntrySelection();
// The one CMsvEntry we should endevour to reuse.
iMsvEntry=CMsvEntry::NewL(*iMessageServer,
KMsvGlobalInBoxIndexEntryId,
TMsvSelectionOrdering());
TBuf<256>bb;
// Scan the inbox looking for SMS messages.
CMsvEntrySelection* inboxEntries=iMsvEntry->
ChildrenWithMtmL(KUidMsgTypeSMS);
CleanupStack::PushL(inboxEntries);
TInt count=inboxEntries->Count();
// Display number of SMS entries we have
// been informed exist.
_LIT(KSMSEntries,"%d SMS entries");
bb.Format(KSMSEntries,count);
iObserver->LogInfo(bb);
// Display who each SMS from + the content as
// an example of acessing these items.
for (TInt i=0;i<count;i++)
{
CMsvEntry* qq=iMessageServer->GetEntryL(inboxEntries->At(i));
TMsvEntry msvEntry=qq->Entry();
delete(qq);
// Demonstrate its an SMS msg thats arrived.
if (msvEntry.iMtm!=KUidMsgTypeSMS)
continue;
// Obtain some info about the SMS in the Inbox.
CSmsClientMtm* smsMtm=static_cast<CSmsClientMtm*>(
iMtmRegistry->NewMtmL(KUidMsgTypeSMS));
CleanupStack::PushL(smsMtm);
smsMtm->SwitchCurrentEntryL(inboxEntries->At(i));
smsMtm->LoadMessageL();
// Look at the SMS header to see who it’s from.
CSmsHeader& smsHdr=smsMtm->SmsHeader();
TPtrC msgFrom(smsHdr.FromAddress());
bb.Format(_L("SMS from: %S"),&msgFrom);
iObserver->LogInfo(bb);
// We can look at the body to see the content.
TPtrC msgBody(smsMtm->Body().Read(0));
bb.Format(_L("SMS body: %S"),&msgBody);
iObserver->LogInfo(bb);
CleanupStack::PopAndDestroy(smsMtm);
}
CleanupStack::PopAndDestroy(inboxEntries);
}
Our first task is to obtain an IPC connection to the message server process. This is closely followed by the requirement to connect to the MTM registry. The CClientMtmRegistry facilitates the creation of the message type specific client MTM objects. These are required to be able to obtain message type-specific information.
Our application creates a CMsvEntry referencing the KMsvGlobalInBoxIndexEntryId. You should recall, a number of folders within the message store have pre-defined IDs. Our application uses this information to create a starting point.
We now request the set of entries belonging to the CMsvEntry, where the child entries’ iMtm field is set to KUidMsgTypeSMS. Since the CMsvEntry is referencing the Inbox, and only SMS messages will belong to the KUidMsgTypeSMS, this extracts all SMS messages within the Inbox.
A CMsvEntry has a number of methods capable of generating a selection:
-
CMsvEntrySelection* ChildrenL() const; -
CMsvEntrySelection* ChildrenWithServiceL(TMsvId aServiceId) const; -
CMsvEntrySelection* ChildrenWithMtmL(TUid aMtm) const; -
CMsvEntrySelection* ChildrenWithTypeL(TUid aType) const;
As you can see, all the variants return a CMsvEntrySelection object. This is simply an array of TMsvIds.
From the CMsvEntrySelection array we can simply iterate though each of the child entries that matched our selection criteria. In this first attempt, we create a CMsvEntry to access the generic properties of the entry described by a TMsvEntry. We also obtain some SMS specific properties, such as the sender and body of a message.
Some optimizations
As previously described, creating CMsvEntrys is a resource intensive operation. Similarly, creating and deleting client MTM objects is inefficient.
Rather than creating and deleting such objects each time around the loop, they are designed to be re-used by updating the entry that they represent. In the case of a CMsvEntry we use the SetEntryL() method: in the case of the CSmsClientMtm we use the SwitchCurrentEntryL() method.
The revised code is:
void CMessageEngine::ConstructL()
{
// Get IPC connection to the message server process.
iMessageServer=CMsvSession::OpenSyncL(*this);
// We need a client side CClientMtmRegistry to obtain MTM objects.
iMtmRegistry = CClientMtmRegistry::NewL(*iMessageServer);
// Required to send SMS entries.
iSendSelection=new(ELeave)CMsvEntrySelection();
// The one CMsvEntry we should endevour to reuse.
iMsvEntry=CMsvEntry::NewL(*iMessageServer,
KMsvGlobalInBoxIndexEntryId,
TMsvSelectionOrdering());
TBuf<256>bb;
// Scan the inbox looking for SMS messages.
CMsvEntrySelection* inboxEntries=iMsvEntry->
ChildrenWithMtmL(KUidMsgTypeSMS);
CleanupStack::PushL(inboxEntries);
TInt count=inboxEntries->Count();
// Display number of SMS entries we have been informed exist.
_LIT(KSMSEntries,"%d SMS entries");
bb.Format(KSMSEntries,count);
iObserver->LogInfo(bb);
// Demonstrates how you should go about
// reusing the CMsvEntry.
CSmsClientMtm* smsMtm=static_cast<CSmsClientMtm*>(
iMtmRegistry->NewMtmL(KUidMsgTypeSMS));
CleanupStack::PushL(smsMtm);
for (TInt i=0;i<count;i++)
{
iMsvEntry->SetEntryL(inboxEntries->At(i));
const TMsvEntry& tEntry=iMsvEntry->Entry();
// Demonstrate its an SMS msg thats arrived.
if (tEntry.iMtm!=KUidMsgTypeSMS)
continue;
// Type should be KUidMsvMessageEntry,
// iServiceId will be KMsvLocalServiceIndexEntryId.
_LIT(KTypeServiceId,"Type: %d, ServiceId %d");
bb.Format(KTypeServiceId,tEntry.iType.iUid,
tEntry.iServiceId);
iObserver->LogInfo(bb);
// Obtain some info about the SMS in the Inbox.
smsMtm->SwitchCurrentEntryL(inboxEntries->At(i));
smsMtm->LoadMessageL();
// Look at the SMS header to see who its from.
CSmsHeader& smsHdr=smsMtm->SmsHeader();
TPtrC msgFrom(smsHdr.FromAddress());
_LIT(KSMSfrom,"SMS from: %S");
bb.Format(KSMSfrom,&msgFrom);
iObserver->LogInfo(bb);
// We can look at the body to see the content.
TPtrC msgBody(smsMtm->Body().Read(0));
_LIT(KSMSbody,"SMS body: %S");
bb.Format(KSMSbody,&msgBody);
iObserver->LogInfo(bb);
}
CleanupStack::PopAndDestroy(smsMtm);
CleanupStack::PopAndDestroy(inboxEntries);
}
The above code demonstrates the recommended approach to using CMsvEntry objects. In particular, it shows how to reuse the object to reference different entries.
Sending Messages
While our example application demonstrates sending an SMS, the general principles involved can be applied to any other message type.
To send a message we need to:
- create an entry in the message store to represent the message
- add any required body text and set up the MTM specific information
- move the entry to the Outbox and schedule the time for it to be sent.
Since a number of the operations involved can take an extended time to complete, most of the functions used to send a message are asynchronous. In a more general interface, some functions need to be able to report progress information, such as reporting which is the current message being downloaded. The message server encapsulates the progress reporting within a CMsvOperation object returned by a number of asynchronous methods.
Creating a new message
The following code starts the construction of a new message:
TBool CMessageEngine::StartSendText(
const TDesC& aMessageBody,
const TDesC& aSmsAddress)
{
if (IsActive())
return(EFalse); // We can’t send the info just yet.
// Reset list of Ids of messages we wish
// to send - as we are creating a new one.
DeleteComponents();
iSendSelection->Reset();
iMessageBody=aMessageBody;
iDestinationAddress=aSmsAddress;
// Setup a blank sms message in the message server.
// The created SMS has no body or destination
// telephone number at this stage.
TMsvEntry newEntry;
newEntry.iServiceId=KMsvLocalServiceIndexEntryId;
newEntry.iRelatedId=0;
// setup the type of the entry: a message.
newEntry.iType=KUidMsvMessageEntry;
newEntry.iMtm=KUidMsgTypeSMS; // It’s an SMS.
newEntry.iDate.UniversalTime();
newEntry.iSize=0;
newEntry.iError=0;
newEntry.iBioType=0;
newEntry.iMtmData1=0;
newEntry.iMtmData2=0;
newEntry.iMtmData3=0;
newEntry.SetInPreparation(ETrue);
TRAPD(err,
// Represents an entry we are manipulating.
iMsvEntry=CMsvEntry::NewL(*iMessageServer,
KMsvDraftEntryIdValue,
TMsvSelectionOrdering());
iOperation=iMsvEntry->CreateL(newEntry,iStatus);
);
iState=ESmsSendCreateMessage;
SetActive();
if (err!=KErrNone)
{ // Simulate request completion on error.
TRequestStatus* q=(&iStatus);
User::RequestComplete(q,err);
}
return(ETrue);
}
The CMsvEntry::CreateL() method is both asynchronous and returns a CMsvOperation. The CMsvOperation can be used to monitor the progress of the message creation. In practice, this operation is quite fast so no specific user feedback needs to be provided. When the message creation completes our RunL() will be called:
void CMessageEngine::RunL()
{
TBuf<128>bb;
TMsvLocalOperationProgress progress;
TInt err=iStatus.Int();
switch (iState)
{
case ESmsSendCreateMessage:
if (err==KErrNone)
{
progress = McliUtils::GetLocalProgressL(*iOperation);
if (progress.iError!=KErrNone)
{ // Some messaging error being reported.
err=progress.iError;
}
else
{
TRAP(err,
// In general we must not assume
// that iMsvEntry has not changed especially
// in an async system so we set it here.
iMsvEntry->SetEntryL(progress.iId);
DeleteOperation();
MsgCreationCompleteL(progress.iId);
);
}
}
if (err!=KErrNone)
{
DeleteOperation();
_LIT(KCreateErr,"Create err %d");
bb.Format(KCreateErr,err);
iObserver->LogInfo(bb);
iState=ESmsSendIdle;
}
break;
// Remainder removed for clarity.
}
}
If we successfully create the message, we ensure that our single global iMsvEntry is referencing the correct entry and proceed to complete the message. At this time the message entry is marked as being in preparation.
void CMessageEngine::MsgCreationCompleteL(const TMsvId aId)
{
if (iMtm==NULL || iMsvEntry->Entry().iMtm != iMtm->
Entry().Entry().iMtm)
{ // We don’t have an mtm or the mtm for this entry
// is different to one we currently have .
delete(iMtm);
iMtm=NULL;
iMtm=iMtmRegistry->NewMtmL(iMsvEntry->Entry().iMtm);
}
// Set indicated entry as current one.
iMtm->SetCurrentEntryL(iMsvEntry);
TMsvEntry tEntry=iMtm->Entry().Entry();
// Set message body from our msg text.
CRichText& mtmBody=iMtm->Body();
mtmBody.Reset();
mtmBody.InsertL(0,iMessageBody);
// Set the destination address.
tEntry.iDetails.Set(iDestinationAddress);
tEntry.SetInPreparation(EFalse);
// We are no longer preparing msg.
// We are now waiting to send.
tEntry.SetSendingState(KMsvSendStateWaiting);
tEntry.iDate.UniversalTime();
CSmsClientMtm* smsMtm = static_cast<CSmsClientMtm*>(iMtm);
smsMtm->RestoreServiceAndSettingsL();
// CSmsHeader encapsulates data specific for sms
// messages, like service center number and
// options for sending.
CSmsSettings* sendOptions=CSmsSettings::NewL();
CleanupStack::PushL(sendOptions);
sendOptions->SetStatusReportHandling(
CSmsSettings::EMoveReportToInboxVisible);
sendOptions->CopyL(smsMtm->ServiceSettings());
sendOptions->SetDelivery(ESmsDeliveryImmediately);
CSmsHeader& header=smsMtm->SmsHeader();
header.SetSmsSettingsL(*sendOptions);
CleanupStack::PopAndDestroy(); // sendOptions
// If no SMC address, attempt to use a
// default SMC address - if none give up.
if (!header.Message().ServiceCenterAddress().Length())
{
CSmsSettings& serviceSettings=smsMtm->ServiceSettings();
// If no SMC - give up here.
if (!serviceSettings.ServiceCenterCount())
User::Leave(KErrCouldNotConnect);
header.Message().SetServiceCenterAddressL(
serviceSettings.GetServiceCenter(
serviceSettings.DefaultServiceCenter()).Address());
if (!header.Message().ServiceCenterAddress().Length())
User::Leave(KErrCouldNotConnect);
}
// Add our recipient to the list, takes in two TDesCs,
// first is real address and the second is an alias –
// works also without the alias parameter.
smsMtm->AddAddresseeL(iDestinationAddress,tEntry.iDetails);
// Save message to server.
smsMtm->SaveMessageL();
// Move the message from the drafts folder
// to the outbox folder.
iMsvEntry->SetEntryL(tEntry.Parent());
iOperation=iMsvEntry->MoveL(tEntry.Id(),
KMsvGlobalOutBoxIndexEntryId,iStatus);
iState=ESmsSendMoveMessageToOutBox;
SetActive();
}
At this stage we set up the body of the SMS to be the content we will transfer. The message is changed from in preparation, and the sending state is set to waiting to be sent. Since the message is an SMS we need to obtain a CSmsClientMtm to be able to set SMS-specific information such as the SMSC address.
Finally, we need to ensure the entry being manipulated is updated server side. Until the call to SaveMessageL() the message information will be client side. The SaveMessageL() method will cause a flush of the updated information server side.
At this point we have a fully constructed message which we need to move from the Drafts folder to the Outbox folder. The move request is asynchronous and so will call our RunL() when it completes.
void CMessageEngine::RunL()
{
TBuf<128>bb;
TMsvLocalOperationProgress progress;
TInt err=iStatus.Int();
switch (iState)
{
case ESmsSendMoveMessageToOutBox:
if (err==KErrNone)
{
progress = McliUtils::GetLocalProgressL(*iOperation);
if (progress.iError!=KErrNone)
{ // Some messaging error being reported.
err=progress.iError;
}
else
{ // Moving to outbox has completed
// sucessfully, start sending the msg.
TRAP(err,
// Add our message to the selection.
iSendSelection->AppendL(progress.iId);
DeleteOperation();
TBuf8<4> junk;
iOperation=iMtm->InvokeAsyncFunctionL(
ESmsMtmCommandScheduleCopy,
*iSendSelection,junk,iStatus);
iState=ESmsSendTransmittingMessage;
SetActive();
);
}
}
if (err!=KErrNone)
{
DeleteOperation();
_LIT(KMoveErr,"Move err %d");
bb.Format(KMoveErr,err);
iObserver->LogInfo(bb);
iState=ESmsSendIdle;
}
break;
// Other cases removed for clarity.
}
}
Once successfully moved to the Outbox we need to schedule the transmission of the SMS. This is achieved by calling the InvokeAsyncFunctionL() method of the CSmsClientMtm. Since this method presents a generic interface, it takes a set of TMsvIds to operate on. While we only have a single entry to send, we still have to pass this encapsulated within a CMsvEntrySelection. As the name implies the InvokeAsyncFunctionL() is asynchronous and therefore calls our RunL() when it completes.
void CMessageEngine::RunL()
{
TBuf<128>bb;
TMsvLocalOperationProgress progress;
TInt err=iStatus.Int();
switch (iState)
{
// Finished ESmsMtmCommandScheduleCopy.
case ESmsSendTransmittingMessage:
DeleteOperation();
_LIT(KScheduledOK,"Scheduled OK");
_LIT(KScheduledErr,"Schedule err %d");
if (err==KErrNone)
bb=KScheduledOK;
else
bb.Format(KScheduledErr,err);
iObserver->LogInfo(bb);
iState=ESmsSendIdle;
break;
// Other cases removed for clarity.
}
}
Note that this means the scheduling operation has completed and not that the SMS has actually been sent yet. The actual transfer will depend on the scheduling information chosen. In our example, we have requested the SMS to be sent immediately since we set the CSmsSettings delivery options to be ESmsDeliveryImmediately and the iDate field to the current time.
When our SMS actually gets sent, we will receive status information from the message server via the HandleSessionEventL() method.
void CMessageEngine::HandleSessionEventL(
TMsvSessionEvent aEvent,
TAny* aArg1,
TAny* aArg2,
TAny* aArg3)
{
if (aEvent==EMsvEntriesMoved)
{
// arg2 is the TMsvId of the new parent.
TMsvId* entryId=static_cast<TMsvId*>(aArg2);
if (*entryId==KMsvSentEntryId)
{
// Items have been moved to the Sent items
// folder. aArg1 is a CMsvEntrySelection (
// list of entries that have been moved).
CMsvEntrySelection* selection=static_cast<
CMsvEntrySelection*>(aArg1);
TBuf<128>bb;
TInt count=selection->Count();
_LIT(KMovedToSentFolder,"%d moved to sent folder");
bb.Format(KMovedToSentFolder,count);
iObserver->LogInfo(bb);
for (TInt i=0;i<count;i++)
{
_LIT(KMovedId,"Moved Id %d");
bb.Format(KMovedId,selection->At(i));
iObserver->LogInfo(bb);
}
}
}
}
In the case of sending entries we will be sent an EMsvEntriesMoved event since the message will be moved from the Outbox to the Sent items folder. The example code shows us processing an EMsvEntriesMoved event. In general, we would record the TMsvIds of the entries we requested to be sent. When we are informed that entries have moved, we can compare the saved TMsvIds with those that have been moved to know whether our entry has been transmitted.
In applications that automatically create and send SMS messages to perform communications activity, you may wish to automatically delete messages after they have been transferred. You need to be quite sure that your messages are uniquely identifiable, and that your application will not delete any other SMS messages. The CMsvEntry:: DeleteL() method can be used to












