Symbian Developer Network

Documentation and Code

ServiceExplorer Demo

Murray Read, Senior Technical Consultant, Symbian
Revision 1.0, February 2008


Introduction

The ServiceExplorer demo shows how to use the Symbian OS v9 Server Application Framework and explores some efficient algorithms in Symbian OS and UI architecture in S60.

Server applications

Server applications introduction

The Server Application Framework allows applications to communicate through client/server inter-process communications (IPC). A client application will launch and connect to a server application. The server application can carry out functions for the client application, with user interaction. The server application may provide new variants of functionality that the client already uses. The server application can also provide functionality that cannot be performed by the client application due to capabilities restrictions, as the server application runs in a separate process.

Clients and servers need to know how to communicate. The Server Application Framework allows them to use well defined services, which encapsulate the IPC messages and provide easy-to-use APIs.

The demo suite

The ServiceExplorer demo allows the user to explore and edit services and server applications available on the phone. It uses features of the Server Application Framework to discover which services and server applications are present. It also defines a service through which server applications can provide more detailed information about particular services. This service is called the Service Info Service.

The main application, called ServiceExplorerApp, lists all the services and all the server applications that are discoverable on the phone. It is also a client of the Service Info Service. To show more detailed information about a service, it uses the Service Info Service to launch an appropriate server application.

A server application, called SvexServiceApp (Svex is a common prefix in this demo), implements the Service Info Service. It knows how to show more information about the Service Info Service (recursive demos are always the best).

Another server application, called SvexOpenApp, implements the Service Info Service and knows about the Open Service – a standard Symbian OS service that concerns the ability of applications to open files by mime-type. Using the Open Service, clients can find applications to open files of a known mime-type and tell these applications to open the file. SvexOpenApp also has the ability to set the default application for handling each mime-type. This ability requires the WriteDeviceData capability and illustrates an important point about server applications – they can have different (more or less) capabilities than their clients. This flexibility makes it easier for independent developers to use powerful functionality (by using server applications) and to extend powerful system applications (by implementing server applications) without necessarily requiring their capabilities.

The final server application, called SvexUnknownApp, implements the Service Info Service and shows generic information for all services. This server application is used to show information about services that no other server application can handle.

Finally, a DLL, called ServiceExplorerLib.dll, contains support for the Service Info Service common classes and UI components.

The Service Info Service

The service support classes

Services normally have a support library implemented in a DLL, which both client and server application implementations can use. The Service Info Service is no exception; it is supported in ServiceExplorerLib.dll.

Service UID

The first thing a service needs is a UID to ensure that client and server applications are having the same conversation. For the Service Info Service, this is defined in SvexService.hrh as follows:

#define KSvexServiceInfoServiceUid 0xA0000549

Service commands

The Service Info Service supports two simple requests:
  1. Show all information about a particular service. Typically this will mean "show all applications that support the service". But in the case of SvexOpenApp, there is a second choice offered by the application: to show all mime-types which can be opened by an application.
  2. Show information about a particular application's implementation of a particular service. This is intended to give a dialog-like behavior from the server application, i.e. when the information is dismissed, UI control returns to the client.
These two requests require command IDs. An aim of writing service support should be to keep the IPC details as private as possible. Therefore the command IDs are defined in SvexService.cpp, as follows:

enum TSvexServiceCmd

    {
    ESvexServiceCmdShowAll = RApaAppServiceBase::KServiceCmdBase,
    ESvexServiceCmdShowApp
    };

All service-specific command ID ranges should start from at least RApaAppServiceBase::KServiceCmdBase.

The service client R-class

Client/server communication needs an R-class for the client to use. The Server Application Framework provides base classes for launch, connect and monitoring support. The S60 variant is RAknAppServiceBase. The Service Info Service's R-class is called RSvexServiceInfo. All service R-classes must implement a ServiceUid() function to return the service's UID. For example:

TUid RSvexServiceInfo::ServiceUid() const
    {

    return TUid::Uid(KSvexServiceInfoServiceUid);
    }


Apart from that, RSvexServiceInfo just has to send messages to the server. For instance sending the ShowApp message looks like this:

EXPORT_C void RSvexServiceInfo::ShowAppL(TUid aServiceUid, TUid aAppUid)
    {
    User::LeaveIfError(
        SendReceive(ESvexServiceCmdShowApp,
                    TIpcArgs(aServiceUid.iUid, aAppUid.iUid)));

    }


One final note about the R-class. To hide the IPC properly, the R-class should not be available to the user code, it should be hidden in a private header file (not done in this demo). Users should use a client interface class, which adds lifetime monitoring of the server application and presents an appropriate API for the client-side of the service.

The client interface class

ServiceExplorerLib.dll contains the API for the client-side of the Service Info Service in a class called CSvexServiceInfoClient. This class offers just two factory functions and a destructor, nothing else! An instance of the class represents the client's relationship with the server application.

The factory functions create an instance of the class for either the ShowAll or ShowApp message. In both cases this involves finding an appropriate server application (see CSvexServiceInfoClient::AppForServiceL and the CSvexServiceServiceInfo class) , launching it and connecting (see the call to ConnectChainedAppL()), and sending the message. A lifetime monitor for the server application is also created. This informs an observer (usually the owner of the CSvexServiceInfoClient) through a MApaServerAppExitObserver interface about when the server application exits.

The server session class

On the other side of the IPC boundary is the session. The Server Application Framework provides base classes for the session, the S60 variant being CAknAppServiceBase. Each service should provide a specialized base class for its sessions. CSvexServiceInfoSession is the base class for Service Info Service sessions. In order to hide the IPC details from server applications, the session class should handle the IPC messages from the client and translate them into virtual function calls for the server application to implement. Here is an abbreviated version of the class declaration showing the virtual functions and ServiceL();

class CSvexServiceInfoSession : public CAknAppServiceBase
    {

public: // for server applications to implement
    virtual void ShowAllL(TUid aServiceUid) = 0;
    virtual void ShowAppL(TUid aServiceUid, TUid aAppUid) = 0;
protected: // from CSession2
    IMPORT_C void ServiceL(const RMessage2& aMessage);
    };


Here is some detail of the ServiceL() function. The showAll message ID results in a call to the ShowAllL() virtual function with a UID extracted from slot 0 of the message. Unhandled messages are base-called.

EXPORT_C void CSvexServiceInfoSession::ServiceL(const RMessage2& aMessage)
    {

    switch (aMessage.Function())
        {

        case ESvexServiceCmdShowAll:
            ShowAllL(TUid::Uid(aMessage.Int0()));
            aMessage.Complete(KErrNone);
            break;
...
        default:
            CAknAppServiceBase::ServiceL(aMessage);
            break;
        }
    }

The server class

Server applications have to provide a server object that can create the correct type of session when the appropriate service UID is requested. It is not necessary for service support to provide any help with this. The Server Application Framework already provides base classes, the S60 variant being CAknAppServer. The Service Info Service does provide some support for the application's server. It converts a request for a Service Info Service session into a virtual function call to return a CSvexServiceInfoSession object. Applications that only implement the Service Info Service can use this class for convenience. Here's an extract, which demonstrates how any server application can create sessions:

CApaAppServiceBase* CSvexServiceInfoServer::CreateServiceL(TUid aServiceType) const
    {

    if (aServiceType.iUid == KSvexServiceInfoServiceUid)
        return CreateServiceInfoServiceL();
    }

Service registration and discovery

Server applications require help in registering themselves for a service. A server application registers its services in its application registration resource file, in the APP_REGISTRATION_INFO resource structure. APP_REGISTRATION_INFO contains a service_list field which is an array of SERVICE_INFO structures. A server application must place a SERVICE_INFO structure into the service_list field for each service it wishes to register. Here is an example of a server application's registration for the Service Info Service:

SERVICE_INFO
            {

            uid = KSvexServiceInfoServiceUid;
            datatype_list = {};
            opaque_data = r_openApp_service_reg;
            }


The opaque_data field contains a link to a resource which gives details specific to the service type, which is defined in a service supplied .RH file. For the Service Info Service, this resource structure is called SVEX_SERVICE and is defined in SvexService.rh. It requires two fields: one for the UID of the service this server application gives information about and another for the engineering name of the service. Here is an example of its use:

RESOURCE SVEX_SERVICE r_openApp_service_reg
    {

    id = 0x10208dca;        // This application understands the Open Service
    name = "OpenService";
    }


Clients need help in discovering what server applications are available for a service. Apparc helps with this, with functions such as RApaLsSession::GetServiceImplementationsLC(). These functions allow identification of all applications that support a particular service ID, but to tell the client how these applications support the service, the service support must be able to interpret the datatype_list and opaque_data fields of the server applications' service registrations.

The class CSvexServiceServiceInfo builds an array of server application implementation details for the Service Info Service. It has a function, ExtractInfoLC(), which extracts the contents of the opaque_data field. To do this, it creates an RResourceReader over the opaque_data field's descriptor and reads it like a resource. RResourceReader is a safer class to use than TResourceReader, as malformed resources will only cause it to leave with an error.
 
Now we have done everything possible to support the use of the Service Info Service, it's time to see how it is actually used in the demo.

Client use

ServiceExplorerApp is the only client of the Service Info Service in this demo. It uses the service in two different ways: to show all information about a service and to show information about a particular application's implementation of a service.

There is a service view, which shows all discoverable services on the phone. This view uses server applications to show all information about a selected service. Since it uses server applications, it wants to monitor their lifetime, so it implements the MApaServerAppExitObserver as follows, through the intermediate class MAknServerAppExitObserver. Note that it is important to base-call to MAknServerAppExitObserver::HandleServerAppExit(), so that standard S60 exit behavior is implemented.

class CServiceView : public CSvexListView, public MAknServerAppExitObserver
    {

    CSvexServiceInfoClient* iServerApp;
    };


void CServiceView::HandleServerAppExit(TInt aReason)
    {

    delete iServerApp;
    iServerApp = 0;
    MAknServerAppExitObserver::HandleServerAppExit(aReason);
    }


When a service is selected from the view's list, the appropriate server application must be found and launched. The service support for the Service Info Service makes this very simple (in this call, "this" has to be a MApaServerAppExitObserver and UID is the service to launch a server application for) :

void CServiceView::OpenL()
    {
    ...
    TUid
uid = iInfo.ServiceUids()[index];
    iServerApp = CSvexServiceInfoClient::NewAllL(..., this, uid);
    }

Server application implementation

There are three demo applications that implement the Service Info Service: SvexServiceApp, SvexOpenApp and SvexUnknownApp. Each one registers as a server application for the Service Info Service in their application registration resource files with the UID of the service that they recognize. Their server and session implementations are virtually identical, they each ask their respective application UIs to show the appropriate views or dialogs.

SvexServiceApp has its session and server classes written as follows (the other two applications are very similar):

class CSvexServiceAppSession : public CSvexServiceInfoSession
    {

public: // from CSvexServiceInfoSession
    void ShowAllL(TUid aServiceUid);
    void ShowAppL(TUid aServiceUid, TUid aAppUid);
    };


void CSvexServiceAppSession::ShowAllL(TUid /*aServiceUid*/)
    {

    AppUi()->ShowAllL();
    }


void CSvexServiceAppSession::ShowAppL(TUid /*aServiceUid*/, TUid aAppUid)
    {

    AppUi()->ShowAppAndExitL(aAppUid);
    }


It has its server class written like this:

class CSvexServiceAppServer : public CSvexServiceInfoServer
    {

public: // from CSvexServiceInfoServer
    CSvexServiceInfoSession* CreateServiceInfoServiceL() const;
    };


CSvexServiceInfoSession* CSvexServiceAppServer::CreateServiceInfoServiceL() const
    {

    return new(ELeave) CSvexServiceAppSession;
    }


There is one other server application-specific task that must be carried out, the server must be created on demand. This happens in an overridden NewAppServerL() function inherited from CApaApplication. Like this (note that this function should only "new" the server, second stage construction happens later in the framework):

void CSvexServiceAppApplication::NewAppServerL(CApaAppServer*& aAppServer)
    {

    aAppServer = new(ELeave) CSvexServiceAppServer();
    }

Private and multi-client server applications

This demo has shown the use of server applications as plugins with varying capabilities. It has also demonstrated how server applications can be registered and discovered through apparc. However, there are other uses of server applications that do not require registration with apparc. Such server applications, possibly very useful, will not be discovered by the techniques used in this demo.

It is possible to have a private server application that can only be used by a single fixed client. This could be a useful technique for application developers in cases where they may want ECom plugin functionality in their application, but also want to carry out one or two high capability functions. The high capabilities required by the application could severely limit the ability of other developers to create plugins due to the signing requirements. However, if the application were split into a low capability client application and a small high capability server application, the low capability application could more easily use plugins. The high capability server application would have to have sufficient security checks in its IPC interfaces to ensure that it could not be abused by a plugin. The server application would only accept connections from the low capability application by checking the application's SID. It could ensure that all important requests through IPC are shown and confirmed by the user before they are actioned.

The most common use of server applications is to launch a new instance of the server app for each client. There can be multiple instances of a server app running simultaneously. The server app framework ensures that each client communicates with its own server app instance. As a consequence, each server app instance has a single client. It is also possible to write a server application that has multiple clients. This can be done by creating a server with a fixed name when the application starts up. Client applications may then connect to the server application using the fixed server name. This technique may be used when there is a long running "system" application which can offer services or mediate shared resources for its clients

Time efficient algorithms

The ServiceExplorer demo has to collect and cross-reference a lot of information from apparc. There is no direct way to discover the set of services that are discoverable through apparc. Instead, you have to query all applications to see what services they support. ServiceExplorerLib provides two classes for getting information about applications and services, CSvexInfoBase uses a direct connection to apparc to get information about individual applications and services. It has a derived class, CSvexInfo, which collects information about all applications and services. The implementation of this and other algorithms in ServiceExplorer provide examples of how the time complexity of long computations can be reduced in Symbian OS v9.

RHashMap

Symbian OS v9 introduces a number of new container classes, RHashMap and its friends in e32hashtab.h (see installation instructions). As the name suggests, these use hashing to trade some memory in order to gain efficient insertion and lookup operations.

The ServiceExplorer demo uses RHashMap in two places: the CSvexInfo class, which has to cross reference services; and SvexOpenApp, which has to invert an array mapping applications to the mime-types they support.

CSvexInfo owns two hash maps. One, iAppMap, allows fast indexing of information about server applications by their UIDs. The other, iServiceMap, allows fast indexing of information about services by their service UIDs. Building this second hash map is the main job of CSvexInfo's GetAllInfoL() function. It gets the applications one-by-one from apparc and inserts them into iAppMap. Then, each service that the application supports is added to iServiceMap by looking for the service UID in the map, inserting a new CSvexServiceInfo if it's not there, and adding the service details to the CSvexServiceInfo. Since all the lookup and insert operations are constant time, this algorithm runs in linear time on the number of applications and services.

These two hash maps are defined as RHashMap<TInt, Csomething*>. TInt is one of the default supported key types for RHashMap, so it makes more sense to use it than TUid.

Sometime we need to iterate through all entries in a hash table. There are iterator classes for each hash container. These iterator classes have NextKey() and NextValue() functions that return pointers to the next key or value, or NULL if at the end. For example CSvexInfo can iterate through all CSvexServiceInfos in iServiceMap as follows:

THashMapIter<TInt, CSvexServiceInfo*> servIter(iServiceMap);
for (CSvexServiceInfo* const* pServ = servIter.NextValue();
     pServ;
     pServ = servIter.NextValue())
    {

    CSvexBuildServiceInfo* const service = (CSvexBuildServiceInfo*)(*pServ);
    ...
    }


SvexOpenApp's CSvexMimeArray class takes an array of applications and the mime-types that they support and turns it into an array of mime-types and the applications that support them. Since it wants to ensure that there is only one entry for each mime-type, it uses a hash map indexed by mime-type. The mime-types are stored as descriptors in the original application array and can be accessed as TPtrC8 descriptors. So, to avoid having to copy the descriptors, we want the hash map's key type to be TPtrC8, but use the default hashing functions for descriptors so that the mime-types are compared and matched as strings. To do this, we need to pass two functions to the hash map's constructor, a hashing function and an identity function. Here's the code:

TUint32 TPtrC8Hash(const TPtrC8& aPtr)
    {

    return DefaultHash::Des8(aPtr);
    }


TBool TPtrC8Ident(const TPtrC8& aL, const TPtrC8& aR)
    {

    return DefaultIdentity::Des8(aL, aR);
    }


RHashMap<TPtrC8, CSvexMimeAppArray*> hash(&TPtrC8Hash, &TPtrC8Ident);

Note: it is tempting to define the hash map as <TDesC8,...>, but this would not be correct as the key types are copied. If you copy a descriptor into a TDesC, you slice it and it becomes meaningless and corrupt (I made this mistake during development of the demo).

Counting and pre-allocation

One of the most commonly used data structures is the array, e.g., CArrayFix<>, RArray<>, etc. One of the most commonly made efficiency errors in Symbian OS is to repeatedly call AppendL() on an array, causing it to repeatedly reallocate the whole array, thereby making the runtime vary as the square of the number of elements appended. This poor performance can almost always be avoided by counting the elements before they are added to the array, reserving enough space for the array, then appending the elements.

CSvexInfo uses this strategy a number of times in its GetAllInfoL() function. It builds arrays of the service and application UIDs it encounters. For both of these, it gets a count of the number of items first (from apparc for applications, or from iServiceMap for services), calls ReserveL() on the UID array, then AppendL() all the UIDs. To create the array of applications that support a service, it uses a more complex variant of the strategy. It initially adds each application to a linked list, counting as it goes. When all applications have been scanned, it reserves array space for the applications, using the count, then goes through the linked list placing each item in the array.

This counting strategy can also help with hash containers. Again in CSvexInfo::GetAllInfoL(), there are a large number of applications that will be inserted into iAppMap. The application count from apparc is used to ReserveL() space in iAppMap before applications are inserted into it. This will prevent unnecessary reallocations internally in iAppMap.

It should also be recognized that it is not always necessary to use the counting strategy. If it is known that only a small number of elements will be inserted into an array or hash map, the overhead of any reallocation will be insignificant. In CSvexInfo::GetAllInfoL(), iServiceMap does not have space reserved, as it is known that there are only a few services. In CSvexMimeArray, it is known that there are very few cases where a mime-type is supported by more than one application, therefore it does not attempt to count applications for a mime-type before adding the applications to each mime-type's array.

These techniques help keep the execution time for CSvexInfo::GetAllInfoL(), to approximately 100ms.

Exponentially growing strings

There is one final technique demonstrated here for which you can occasionally find uses: the exponentially growing string accumulator. The demo applications use dialogs to show textual information about application's registrations for services. This textual information is built by joining a variable number of substrings of different sizes into one larger string. This could easily suffer from the same reallocation problems as appending to arrays where the runtime can vary as the square of the number of characters. We could use the strategy of creating a list of all the substrings before building the final string, but that would take a lot of code and we're feeling lazy! So we use an exponentially growing string accumulator, which reduces the runtime to what is called O(n), i.e. quite fast. Here's the key function:

EXPORT_C void CAppServiceInfoDialog::AppendTextL(const TDesC& aText)
    {

    while (aText.Length() + iBuf->Length() > iBuf->Des().MaxLength())
        iBuf = iBuf->ReAllocL(iBuf->Des().MaxLength()*2);
    iBuf->Des().Append(aText);
    }


This keeps an accumulator buffer, iBuf, which is doubled in size whenever it does not have enough room for the text to be added. It does cause unnecessary space to be allocated for the string, approximately 40% more on average. But, it can occasionally be a good trade off between RAM, ROM, CPU and developer time.

User interface

Layout change

The ability to change layout when required to do so is important for applications since S60 2.8. This demo uses a control containing a list for all of the applications' main views. When it is used as a main view, it has to place itself in the screen area called the main pane, it has to be window-owning and it has to respond to layout change events. Layout change events are picked up in the HandleResourceChange() method and the layout change is effected as follows:

EXPORT_C void CSvexListView::HandleResourceChange(TInt aType)
    {

    if (aType == KEikDynamicLayoutVariantSwitch && OwnsWindow())
        {

        TRect rect;
        AknLayoutUtils::LayoutMetricsRect(AknLayoutUtils::EMainPane, rect);
        SetRect(rect);
        }

    CCoeControl::HandleResourceChange(aType);
    }


The message, KEikDynamicLayoutVariantSwitch, is the key to detecting layout changes. The UI framework will send it to all application UIs, control stack controls and their contained controls when layout changes. Applications are responsible for forwarding the message to any top-level controls not on the control stack. This control only responds to the message when it is a window owning control, which is only the case when it's used as a main view. When this control is used as a contained control, its parent is responsible for setting its size. The API, AknLayoutUtils::LayoutMetricsRect, can be used to get layout rectangles for various interesting parts of the UI. SetRect() is used to set this control's size, which in turn calls the SizeChange() function, which has to set the size of the contained list control. Finally, we base call.

UI architecture

The demo applications have non-trivial UI structure. Some applications have with multiple top-level views, all have nested views. The server applications act as nested views for ServiceExplorerApp and all views have normal S60 "back" and "exit" behavior. This is implemented as follows:
  • The top level views are CCoeControl-derived. This gives more flexibility and security than CAknView or MCoeView. The only disadvantage is that CAknView provides menu and soft key support. Command and menu handling support was an easy addition to the CSvexListView base class used for all top level views in this demo, with the virtual functions HandleListBoxEventL() and HandleCommandL().
  • Nested views within an application are all implemented with dialogs. Dialogs are the most practical way to implement nested views in S60.
  • The server applications appear to be nested inside ServiceExplorerApp because they are launched with the ConnectChainedAppL() method. A server application can also be launched with a stand-alone appearance by using ConnectNewAppL().

Building, installing and testing

The ServiceExplorer demo can be built for emulator and hardware in an S60 3.0 SDK for Symbian OS. First install e32hashtab.h, then build ServiceExplorer as follows:

cd \ServiceExplorer
buildAll.cmd bldmake bldfiles
buildAll.cmd abld build winscw udeb
buildAll.cmd abld build armv5 urel

After building, it is ready to be run on the emulator. A hardware installation package file, ServiceExplorer.pkg, is provided for creating sis files.

To explore all services, run ServiceExplorerApp. SvexOpenApp and SvexServiceApp can also be launched as stand-alone applications for exploring their particular service functionality. ServiceExplorerApp and SvexOpenApp have menu options for changing views. SvexOpenApp has a menu option, when viewing the details of a mime-type, for setting the default handler application.

E32hashtab.h

At the time of writing, the header file e32hashtab.h is not in the S60 3rd Edition SDK for Symbian OS. It will be included in the SDK in the future. In the meantime, this demo supplies the header, which can be installed as follows:

cd \ServiceExplorer\Symbian\group
bldmake bldfiles
abld export

Conclusion

Server applications and services allow flexible inter-application communications with the security and benefit of both parties running in separate processes.
A number of techniques can be commonly used to write fast Symbian OS code. In particular, RHashMap and friends allow fast code to be developed easily and rapidly.
User interfaces created from CCoeControls, dialogs and server applications can be highly structured and UIs can respond to layout changes when HandleResourceChange(KEikDynamicLayoutVariantSwitch) is called on them.