
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:
- 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.
- 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.