Building an application
From UIQ Books
Building An Application
In previous chapters we have built some example applications to explore various aspects of the UIQ platform. In this chapter we build on that knowledge with the primary objective of creating a GUI application, SignedApp, capable of passing the Symbian Signed Test Criteria which are presented in the Chapter Symbian Signed. As the application is developed, the impact of Symbian Signed is explained and the required metafiles are described.
The first stage, SignedAppPhase1, provides a generic application framework that can be adapted to any application that is intended to be Symbian Signed.
Next, we describe how to utilize the generic framework in building the SignedAppPhase2 application. We add application-specific code including an application engine, category management, list view and detail view. We explain the use of INI files, externalization and internalization, with emphasis on the use of streams and stores. We then discuss preparations for submission to Symbian Signed. In the next we develop the SignedAppPhase2 application to SignedAppPhase3 by adding some multimedia functionality.
Symbian Signed
Symbian Signed is a scheme run by Symbian and backed by the mobile industry. Symbian Signed applications follow industry-agreed quality guidelines and support Network Operator requirements for signed applications. Symbian Signed enables you to certify your application to be installed and run on UIQ 3 and S60 phones.
Symbian Signed is described in detail in the Chapter Symbian Signed, however, it is very important to consider your approach to Symbian Signed before you start coding. For most options in Symbian Signed, you will need a Publisher ID. This is a digital certificate that is issued by a third party trusted authority. It verifies your identity to the Test House and to end users installing your application.
Capabilities
Prior to coding, you will usually write some form of functional specification which sets out the functions and features of your application. Generally your specification includes details of how an application should integrate with the mobile phone environment. It is important that you consider how easy it is to implement the required functions:
- Are the APIs available for the features you wish to include?
- Does your application perform functions that are considered to be sensitive and are protected by Symbian OS?
Capabilities define which protected APIs within Symbian OS v9 your application can successfully access. Without the required capability an API usually returns an error (KErrPermissionDenied == -46) and does not perform the requested task. The SDK documentation indicates required capabilities for protected APIs.
Capabilities are divided into three groups:
- User, also known as Basic. The user can grant the capability to an unsigned application via an on-screen dialog box. A Symbian Signed application is automatically granted the required capabilities with no user interaction.
- System, also known as Extended. The application must be Symbian Signed to get these capabilities.
- Device Manufacturer. You must have manufacturer support and Symbian Sign the application.
As we develop our SignedApp example, we add functions that require user capabilities. Symbian Signed is the best way to grant these capabilities, and is the approach that we have chosen.
You should review the functionality in your application and check for the capabilities that you will need.
UIDs and Symbian Signed
The example applications in the previous chapters use UIDs in the range 0xE0000000 to 0xEFFFFFFF. These values are reserved for internal development purposes only. Any applications that are to be distributed should not use these UID values. In particular there is no formal mechanism to ensure two developers do not use the same values from this particular range.
If you have chosen not to get your application Symbian Signed you must obtain a UID from the Symbian OS v9 unprotected range 0xA0000000 to 0xAFFFFFFF for your application UID.
If you have chosen to get your application Symbian Signed, your first requirement is to obtain a UID from the Symbian OS v9 protected range 0x20000000 to 0x2FFFFFFF. If you change your mind and later decide you no longer want to go through the Symbian Signed tests, you have to change the UID since the application installer refuses to install applications with UID values in the protected range unless they are signed.
How to Get Symbian Signed
We completed our SignedApp project before the introduction of the new Open, Express and Certified Signed schemes in late 2007. Had those options been available, we probably would have chosen Express Signed, which grants access to the capabilities we need with the minimum overhead. In the event we chose to follow an option equivalent to Certified Signed, in which we used our Publisher ID and an external Test House. The various ways you can get your application Symbian Signed are explained in Chapter Symbian Signed.
Both Express and Certified Signed require a Publisher ID. Certified Signed also requires independent testing; a Test House performs the testing and facilitates the signing of our application using the Publisher ID. This process typically takes one week; we recommend that you allow at least two weeks for your first project. You will also have to accept legal agreements, which can take additional time if company lawyers become involved.
Symbian Signed Test Cases
All Symbian OS applications are expected to conform to basic criteria of soundness and stability, defined by Test Criteria. Symbian Signed provides a test specification document detailing the tests to be performed and the expected results. Before embarking on a project you should read the Symbian Signed Test Criteria document, available from the Symbian Signed website, to ensure your functional specifications and the Test Criteria are achievable.
If you follow the Certified Signed option, these are the criteria the Test House will test against. If you follow the Express Signed option, you self-certify test results based on these criteria.'
Important Note on Building the Applications
You can freely build the SignedApp example, with or without code changes, and run it in the UIQ emulator. Please note that we use the same application UID in each of the phases. This approach means that only one of the phases can be installed at any time.
SignedAppPhase3 has been Symbian Signed and we provide the signed SIS file in the example download so that you can install it to a mobile phone.
To test SignedAppPhase1, SignedAppPhase2, any modification to or rebuild of SignedAppPhase3 or your own application on a real mobile phone, you must use the Open Signed method. This uses developer certificates that are restricted to specific phones based on IMEI number.
Unfortunately, this also means that we are unable to include a suitable developer certificate in the example download, since Publisher IDs and therefore developer certificates are issued to named companies or organizations. To build and deploy the SignedApp examples, you will need to follow the Open Signed process. Section #Building_your_Application_for_Deployment explains how we prepare SignedAppPhase2 for testing on real hardware.
Starting Our Project: SignedAppPhase1
Our first task is to generate the project framework for our application. IDEs provide wizards to automate some of this work; however we generate the framework for SignedAppPhase1 manually in order to help you to understand the components involved.
There is little substance to the application itself; the application space is blank with a single menu command The main thing is that it builds cleanly and runs on both the emulator and mobile phone. With replacement of the UID and renaming of the files, this framework can be applied to almost any application development.
Figure 1 SignedAppPhase1
Project Framework
Our previous examples used a very simple structure in which all files were held together in one folder, with just one subfolder for images. We now introduce the recommended folder structure which is commonly used for UIQ 3 application development. This structure is not mandatory, however it is widely used and you will benefit from adopting a well-defined environment.
Folder Structure
The folder structure for SignedAppPhase1 looks like this:
Figure 2 Application folder structure
The main change from our earlier example application is that the project maintenance files have been moved to the Group folder, and the C++ code to the Src folder. In addition:
Data
The Data folder contains two sub-folders, one for the application icon and one for other graphics.
English
Separate folders are used for each supported language. English contains files with the English language text for our application. Later we show how to add additional languages to your application.
Group
This folder contains all the project maintenance metafiles.
Inc
Inc contains the header files that need to be shared between the C++ code and resources.
Src
Src contains our application code. Currently our application is trivial and consists of a single source file and its associated header file. Later we split this into various components.
| Pro tip:
It is good practice to make public only those entities that need to be public. Therefore:
|
Language Folders
The English folder contains two files:
-
SignedApp.rls -
SignedApp_loc.rls
The content of these files is a series of statements like this:
rls_string STR_R_CMD_ABOUT "About"
These statements are simply defining a set of tokens to have a value, in this example the STR_R_CMD_ABOUT token is assigned the value ‘About’. Our previous example applications simply contained the text in-line within the resource files. Using token-value pairs like this makes it easier to manage translations.
In the Chapter Refining your application, in our localization example, we add folders and text files for French and Simplified Chinese.
Group Folder
The Group folder contains the project maintenance and metafiles.
backup_registration.xml
The system backup mechanism looks for a file named backup_registration.xml within the private folder of your application. This file defines how your application interacts with the system backup.
Our backup registration file specifies that the application data is fully contained within its private data-caged folder and both it and the application should be backed up automatically by the system. This means that our application does not have to actively participate in backup.
The concepts of data caging and private folders are discussed in the Chapter Symbian OS essentials. The Symbian Signed test CON-04 in Test Criteria v2.11.0 (UNI-07 in Test Criteria v3.0.0) covers backup behavior and we discuss it in the Chapter Symbian Signed.
bld.inf
As with our previous examples, this informs the Symbian build environment for which targets it should create makefiles. It also specifies the MMP filename
bldarm.bat
This is a simple DOS command line batch file that automates building the Arm version of our application using the GCCE compiler. An IDE would normally be able to perform this task with the click of a button.
bldall.bat
Building the executable program by using the bldarm batch file is a core task but it is not the only task required to construct an application suitable for deployment. In particular you need to:
- package all your files into a single SIS file
- sign the SIS file so it can be deployed to a real mobile phone for test purposes
- sign the SIS file so it can be submitted to the Test House.
This batch file encapsulates the entire build process for our application. We describe the content of this file later. In general an IDE only performs part of this task, building the application. Packing and signing is not fully supported.
| Pro tip:
In Carbide 1.2, the project settings only allow you to specify a single certificate and key pair. Usually two certificate and key pairs are required, one for the SIS file you test and one for the SIS file you submit for signing. Therefore you need to change the settings and re-create the SIS file before submitting to a Test House or Express signing. To eliminate any possible errors we recommend using a batch file such as |
SignedApp.mmp
This is the application master project file. We will look at this file in more detail later on.
SignedApp.pkg
This is the application package file, specifying various attributes of your application including the list of files you wish to install to a target mobile phone.
SignedApp_0x20000462.rss
This file contains the application-specific resources. We have worked extensively with resource files in the preceding chapters.
SignedApp_loc_0x20000462.rss
This file contains the application registration localisation information and is discussed in Section #Application_Registration_Localisation_Files.
SignedApp_reg_0x20000462.rss
This contains the application registration information and is discussed in Section Application Registration Files.
Naming Resource and Executable Files
It is not currently a Symbian Signed test requirement but a recommended best practice is that the binary files (such as EXEs and DLLs) of your application have a unique name.
In versions of Symbian OS prior to OS v9 most application executables were placed in their own folders but with the advent of the security platform in Symbian OS v9, all executable files must be placed in the same folder, \sys\bin. If the executable is not in that folder, the OS will not load or run it. Name clashes are therefore much more likely than was the case with previous versions of Symbian OS.
To avoid name clashes, it is recommended that any executable should have the application UID appended to the executable name. An end-user is highly unlikely to ever see the executable filename so there should not be any presentation issues.
There are two reasons why this affects resource filenames:
- Firstly, as the registration and standard resource files are also placed in common folders, any name clash that could occur with binary files can also occur with these files. If these files cannot be installed or overwrite files where the name clashes then problems will occur
- Secondly, the application framework code derives your resource and multi-bitmap filename from the name of the binary file. Since the framework code opens these files on your behalf if it cannot derive the filename it will fail to open the files.
It is not mandatory to name your files like this, but as Symbian Signed evolves it may become mandatory. If you submit applications to a Symbian Signed Test House and do not name binaries correctly, this is noted in the test report. We strongly recommend you adopt these naming policies.
| Pro tip:
Various files in a Symbian OS project are automatically generated, in particular MBG and RSG files. The full name of these files is generated from the incoming bitmap and resource filename respectively and is referenced by other source files in your project. If you change the names of your files mid-project remember that changing all references is time consuming and prone to errors. |
You may believe that you can rename the files to contain the application UIDs at the last stage, when you generate the PKG file. This is true for some of the files, for example the EXE. However, if you look at the content of the SignedApp_reg_0x20000462.rss file you see it references SignedApp_loc_0x20000462. Simply renaming files in the PKG file will not cause the compiled content of the registration resource file to reference the correct file. Therefore we strongly recommend that you adopt the file naming convention from the beginning of the project.
Application Registration Files
The application registration file contains attributes of the application. The application launcher reads it to find out about the application.
Our application registration file contains the following:
// File type UID.
UID2 KUidAppRegistrationResourceFile
// Application UID.
UID3 0x20000462
RESOURCE APP_REGISTRATION_INFO
{
// Filename of application binary (minus extension).
app_file = "SignedApp_0x20000462";
// Location of the localisable icon/caption
// definition file.
localisable_resource_file = "\\Resource\\Apps\\SignedApp_loc_0x20000462";
}
Developers familiar with S60 3rd Edition may notice this file appears to differ from the S60 equivalent. For example, the localisable_resource_id field does not appear to be defined. As with many resources the above utilizes a number of default values. The localisable_resource_id default value is 1.
Depending on your organization’s preferences you may wish to generate the <SignedApp_loc_0x20000462.rsg> header file, include it within this file and assign to the localisable_resource_id field the same name as assigned to the LOCALISABLE_APP_INFO resource, described later.
localisable_resource_id = R_APPLICATION_INFO;
Similarly the application takes on various other default properties such as:
-
KAppNotHidden -
KAppNotEmbeddable -
KAppDoesNotSupportNewFile -
KAppLaunchInForeground.
Again you may prefer to explicitly define these values for example:
embeddability = KAppNotEmbeddable; newfile = KAppDoesNotSupportNewFile;
You should refer to the UIQ 3 SDK documentation for the full range and meaning of these attributes.
Rather than relying on filename extensions, Symbian OS usually interprets files by their content. Most files native to Symbian OS contains a four digit header comprising three UIDs and a checksum. This provides a fast and reliable way of identifying content.
Compiled application registration files contain this header, the UID2 and UID3 statements provide the second and third digits. As previously explained, UID2 only has meaning in the context of UID1, and UID3 in the context of UID2.
Application registration file content is defined by the application launcher. It has defined UID2 to be the value KUidAppRegistrationResourceFile (0x101F8021) so it knows this file is an application registration file as opposed to any other file type. It has further defined UID3 of the file to take the UID of the application the registration file with which it is associated.
The app_file statement specifies the name of the application executable. Since all executables must reside in the \sys\bin folder further information about its path is not required.
Many applications present language-dependent, or, more precisely, locale-dependent, information to a user. Both icons and text can contain locale-specific information. The localisable_resource_file statement specifies where to find this information.
Application Registration Localisation Files
The SignedAppPhase1 application registration localisation file contains the following content:
#define EViewIdPrimaryView 0x00000001
#ifdef LANGUAGE_01
#include "..\English\SignedApp_loc.rls"
#endif
// This file localises the applications icons and caption.
RESOURCE LOCALISABLE_APP_INFO
{
short_caption = STR_R_APP_SHORT_CAPTION;
caption_and_icon =
{
CAPTION_AND_ICON_INFO
{
caption = STR_R_APP_LONG_CAPTION;
number_of_icons = 3;
icon_file = "\\Resource\\Apps\\SignedApp_icons_0x20000462.mbm";
}
};
view_list =
{
VIEW_DATA
{
uid=EViewIdPrimaryView;
screen_mode=0;
caption_and_icon =
{
CAPTION_AND_ICON_INFO
{
}
};
},
VIEW_DATA
{
uid=EViewIdPrimaryView;
screen_mode=EQikScreenModeSmallPortrait;
caption_and_icon =
{
CAPTION_AND_ICON_INFO
{
}
};
}
};
}
As with application registration files, developers familiar with S60 3rd Edition may notice some differences. For example, no name is assigned to the LOCALISABLE_APP_INFO resource. In S60 3rd edition, the name is used within the application registration file and so is required. In UIQ v3 we simply use the default value, 1. In contrast S60 3rd Edition does not utilize the view_list fields. It uses the default values.
Application captions
At the top of the resource you can see the short_caption and caption_and_icon fields. A caption is the application name presented to the user. The caption_and_icon structure defines the normal caption along with information about the application icons that can be used. In general, the short caption is used when the normal caption does not fit on the display.
Rather than containing quoted text the caption names are represented by tokens. These tokens are converted into text when the file is compiled. The token value pairs are defined in the #include ‘..\English\SignedApp_loc.rls’ file. As you may be able to guess, when alternate locale versions of the file are compiled the tokens change to take on alternate locale-specific values.
| Pro tip:
If you are only ever going to produce a single locale version of your application it is possible to replace the tokens with inline quoted text. However, we would still recommend using the token-based approach as it is negligible extra work at this stage and makes it easy for you to add additional locales later on. |
Application Icons
Our application needs three icons and transparency masks so that it can be displayed correctly in each application launcher state. For the application list view, we need an icon that is 18x18 pixels in size:
Figure 3 Application icon in application launcher list view
A 40x40 pixel icon is used in the grid style launcher and in the application title bar; a 48x48 pixel icon is used when highlighted.
Figure 4 Application icon in application launcher grid view
The transparency mask specifies which part of the icon is visible (white area in the mask) and which is transparent (black area). You can also use an 8-bit greyscale mask and define areas that are semi-transparent. We define the compilation of these images into a single multi-bitmap (MBM) file in the MMP file.
You may wish to note that all masks in UIQ 3 are backwards compared to other Symbian platforms, including UIQ 2.
From UIQ 3.1 you can use SVG icons in the application launcher.
The view_list
Some UIQ 3 mobile phones are able to support multiple view configurations. For example, the Sony Ericsson P990i uses Pen Style configuration when the flip is open and, Softkey Style Small, when the flip is closed. To inform the Sony Ericsson P990i that your application supports Softkey Style Small it is not enough to simply define a KQikSoftkeyStyleSmallPortrait view configuration in your primary resource file. You also have to tell the application launcher that it should display your application icon when the phone is in Softkey Style Small UI configuration. The VIEW_DATA structures inform the application launcher how your application supports the default and EQikScreenModeSmallPortrait screen modes.
| Pro tip:
If you choose not to support the Softkey Style Small mode of the Sony Ericsson P990i in your application, not only do you need to omit the second |
Application MMP File
A number of fundamental changes have been made with the introduction of Symbian OS v9. Perhaps the most significant of these is that applications are no longer polymorphic DLLs which are loaded as plug-ins to a common framework (the AppRun executable). They are now full executables in their own right.
Apart from some API changes, the most significant aspect of this change is that those properties of an application that were defined by the single common framework, such as stack and heap size, are now available for individual applications to define. While many of the common framework properties took on the system wide default values, some values were changed. Individual applications may want to consider changing these values as well. As with previous versions of Symbian OS these properties are defined through the application MMP file. Symbian OS v9 has also introduced some additional properties for an executable, most notably the platform security capabilities.
The SignedAppPhase1 application contains the following within its MMP file:
TARGET SignedApp_0x20000462.exe TARGETTYPE exe UID 0x0 0x20000462 EPOCSTACKSIZE 0x5000 CAPABILITY NONE
As with UIQ 2 we define our primary target filename with the TARGET statement. Applications have changed to be of type EXE; the TARGETTYPE statement reflects this change.
Application UIDs
As we previously discussed, Symbian OS has a strong tendency to interpret files by their content rather than extension. In particular, Symbian native files usually have signature comprising three UIDs and a checksum. The TARGETTYPE statement is used to generate the value for UID1.
Prior to Symbian OS v9 applications were polymorphic DLLs. They were required to have UID2 set to 0x100039CE to be recognized by the common application framework as a DLL that implemented the application-specific polymorphic DLL interface.
In Symbian OS v9 applications are executables in their own right. They no longer need to identify themselves as implementing a particular plug-in interface since they don’t plug-in to any other component. UID2 should now be set to 0.
UID3 takes the application unique ID.
EPOCSTACKSIZE
The system wide default value for the stack size is 0x2000 bytes (8 KB) bytes. This value is defined within the Symbian OS tool chain that creates executables.
Prior to Symbian OS v9 the common application framework set the EPOCSTACKSIZE to a value of 0x5000 (20 KB). Your application therefore ran with a stack size of 20 KB on earlier versions of UIQ.
From Symbian OS v9 applications are executables in their own right and can therefore redefine their stack size if they need more memory.
What factors to consider for stack size?
In general, default values should be used unless there is good reason to change them. Stack overflow issues are very hard to track down, however. For example, if you are porting known working applications from previous versions of UIQ or S60 they work with a stack size of 20 KB. Using the default of 8 KB is probably taking an unnecessary risk.
| Pro tip:
Stack overflow can sometimes manifest itself as a |
If you are writing new applications, but using similar or shared functionality to previously known working applications, then setting the stack size to 20 KB is also reasonable.
Text on UIQ 3 mobile phones is stored as Unicode. The original 8 KB stack size was set in the tool chain when single byte character sets was the standard build variant. A single filename buffer comprises 256 Unicode characters or 512 bytes. With an 8 KB stack an absolute maximum of 16 filename buffers can be stored. In reality it is less as the stack is also used for various system run time information as well as all the other automatics. Perhaps 10 or 12 filename buffers would be the maximum, compared to the original design choice of around 20 buffers.
Adding code to reduce stack usage can needlessly complicate applications. For example, if all filename buffers had to be allocated from the heap, your application would need to manage those memory allocations and deal with potential allocation failures. This can easily add several kilobytes of code as well as still requiring the memory storage, thus the potential memory saving benefits of a smaller stack are eroded rapidly.
Moving buffers previously allocated on the stack into object property can needlessly increase the size of object property. Unlike stack based variables, the memory occupied by object property is not normally re-usable. In addition, simply saving a few kilobytes from the stack by moving it to object property does not change the overall memory resource usage of an application, it simply redistributes the memory amongst the different, logically separated, regions of memory an application uses.
If written correctly a large proportion of your UIQ v3 application comprises of system supplied API calls. It is difficult to know how little or how much stack they use, particularly if they manipulate text or filenames. In general, they are frugal, however there are no explicit tools available to measure stack usage. To err on the size of caution is to be recommended.
Current UIQ 3 phones have more than 20 MB memory. Even at 20 MB, a 20 KB stack is just 0.1% of available memory.
| Pro tip:
With practice, and with some care, it is relatively easy to write applications restricted to a 20 KB stack. In contrast writing applications restricted to an 8 KB stack is somewhat harder. Therefore, if you feel that using the default 8 KB stack size is likely to be a bit too restrictive we recommend that a 20 KB stack be used as opposed to any other value. |
In exceptional circumstances you can define your stack size to be larger, however, you must exercise care. Whatever value you choose, the system reserves that amount of memory, making it unavailable to other applications. For example if you choose to have a 1 MB stack but only ever use 50 KB then the remaining 950 KB is unusable by the rest of the system. As a developer on any platform, but particularly one with limited memory resources, you are responsible for efficiency and good behavior in your application to ensure the system and ultimately the user is not adversely affected.
CAPABILITY statement
The CAPABILTY keyword in the MMP file is used to list the set of platform security capabilities that our application needs in order to perform its functions. Our application does not require any capabilities yet since we have not added any code that accesses protected APIs.
It is strongly recommended that you start with:
CAPABILITY NONE
And then add capabilities as and when you determine they are required.
| Pro tip:
Some developers suggest that you start with a
This says you want all capabilities except for the As an application developer you need to understand which APIs used by your application requires which capabilities. In particular, when you come to submit an application via the Symbian Signed website you need to complete a declarative statement explaining which system capabilities your application uses and why. In addition, the above statement also includes a number of the system capabilities such as |
Application Icons
The three applications icons and masks that we saw in the previous section must be compiled in to a single MBM file. The following code in the MMP file does this:
// Application icons START BITMAP SignedApp_icons_0x20000462.mbm SOURCEPATH ..\Data\AppIcon TARGETPATH \Resource\Apps SOURCE c16 AppIcon18x18.bmp SOURCE 1 AppIcon18x18mask.bmp SOURCE c16 AppIcon40x40.bmp SOURCE 1 AppIcon40x40mask.bmp SOURCE c16 AppIcon64x64.bmp SOURCE 1 AppIcon64x64mask.bmp END
You should always define masks as either 1 (black and white) or 8 (bit gray scale) so that they work correctly. The icons are defined as c16, 16-bit color.
You can find out more about MMP files if you work though the examples presented earlier in this book.
Compiling Your Application
The source files that we have discussed are compiled in to a PKG file, as shown below. The MakeSIS tool creates a SIS file. Because we are using a protected range UID, you must first sign the SIS file before it can be installed on a mobile phone. Section #Building_your_Application_for_Deployment describes how to deploy for testing.
Figure 5 Compiling to a PKG file and creating a SIS file
SignedAppPhase2
SignedAppPhase2 takes the basic project framework that we created in SignedAppPhase1 and adds functionality to make a simple file manager application with the following features:
- display a list view of the files present in folder
c:\Private\20000462 - sort the list by name, size, type, date
- filter the list by category
- display a detail view of filename, date and category and allow these to be updated
- provide a Send As facility to transfer files
- provide a registration feature using IMEI.
To help you understand the design and coding process, here are some screenshots of the final SignedAppPhase2 application:
Figure 6 SignedAppPhase2 list and detail views
Figure 7 SignedAppPhase2 menu showing category and sort submenus
Prior to implementing the code for our application we need to have an application design that implements the functional specification. You may have company procedures that tell you how to do this. For our application we have chosen a minimalist design strategy, comprising a set of statements and a couple of diagrams.
The application comprises:
- an engine responsible for maintaining the data structures we wish to display,
Engine.cpp - a list view responsible for displaying a sorted and filtered list of entries,
ListView.cpp - a detail view responsible for displaying and editing the details of a specific entry,
DetailsView.cpp - a set of dialogs and some general application-framework code,
SignedApp.cpp.
Figure 8 Application structure
Application Engine
The application engine is responsible for:
- generating the list of files within a particular folder on the phone
- maintaining the set of categories defined within the application
- sorting and filtering entries dependant on the users chosen sort order and category.
Sorting and Filtering Entries
You may recall that the Commands1 example application showed how we might use the list box to sort entries. We have chosen to move the sorting and filtering functionality into the engine for a number of reasons:
- List boxes do not support filtering by category very easily; you have to rebuild the list box from an underlying data structure. Entries that do not belong to the current category have to be stored outside of the list box – we have to have some kind of engine.
- The sort comparison function in the
Commands1example is inefficient. Every comparison requires a lookup from an externally stored variable, via thread local storage in the case of theCommands1application, to obtain a reference to the engine to know how to actually perform the sort. The alternative is to supply different sort comparison functions, one for every possible variation supported. - Sorting by more than a single key is difficult to implement since there is no easy access to second order key information.
- Sorting can leave. This requires you implement some error handling and rollback, which is particularly unfavourable when other operations, such as rename operations, are considered.
In our implementation we generate a set of all entries within a folder and store these within an RArray. Each entry is represented by a TFolderEntry object. We then generate an index referencing these entries.
To produce a filtered list, we simply build an index that references a subset of the underlying entries; the underlying entries remain static. To sort the underlying entries we sort the index rather than the entries themselves. To produce a sorted and filtered list we simply apply the two operations to the index.
Figure 9 Using an index to reference a full list
The advantages of this approach are that:
- Filtering the list by category is quite simple. All that is required is to create an index that references only those entries belonging to a specific category. If the chosen category is All then all the entries exist within the index.
- The sort function is very efficient and properly encapsulated. By using an appropriate class we can remove any external variable lookup and perform multi-key sorts very easily.
- Only a single comparison function is required for all possible sort variations.
- The swap function is very efficient; we only need to swap index entries and not the underlying data structures.
- The sort operation cannot leave.
The following method generates the new folder list, creates an index containing those entries within the currently selected category, sorts the index using the currently selected sort order and finally replaces the existing data structures with the new structures. This last operation is important to ensure we can rollback with the previous application state remaining intact. For example if we simply replaced the iFolderEntryList with the new list before we successfully created a new index on this list, there are no guarantees the old index is valid for this new list.
void CAppEngine::BuildDirectoryListL(const TDesC& aPath)
// Get a list of the files and folders
// within the indicated folder.
{
TParse parse;
parse.Set(KWildCardName,&aPath,NULL);
CDir *dirList;
User::LeaveIfError(iFs.GetDir(parse.FullName(),
KEntryAttNormal|KEntryAttReadOnly|
KEntryAttHidden|KEntryAttSystem|KEntryAttDir,0, dirList));
// Ensure some part of the app takes ownership of
// the dirList before we can leave later on.
CleanupStack::PushL(dirList);
// Create a new container to store the entries.
RArray<TFolderEntry>*folderEntryList=new(ELeave)RArray<TFolderEntry>(8);
// Not only do we need to ensure we delete
// the actual allocated Rarray.
CleanupDeletePushL(folderEntryList);
// But we also have to ensure we call Close()
// before the delete to release any resources.
CleanupClosePushL(*folderEntryList);
// Add a set of entries to our new container.
TFolderEntry entry;
const TInt count=dirList->Count();
for (TInt i=0;i<count;i++)
{
// Construct a new entry.
entry.Construct((*dirList)[i]);
// Add to the new list of entries.
folderEntryList->AppendL(entry);
}
// We need an index onto the entries
// to perform efficient processing.
CArrayFixFlat<TInt>* index=BuildIndexL(folderEntryList,iCurrentCategory);
// Now sort the array of TInts dependant on
// the content they refer to, in the current
// mode + ascending/descending order.
TkeyEntryList key(folderEntryList,iSortType,iSortOrder);
index->Sort(key);
// Replace any existing list with the new list.
if (iFolderEntryList)
{
iFolderEntryList->Close();
delete(iFolderEntryList);
}
iFolderEntryList=folderEntryList;
// Firstly the Cleanup stack item that calls close().
CleanupStack::Pop(folderEntryList);
// Then the cleanup stack item that deletes
// the actual object.
CleanupStack::Pop(folderEntryList);
// Replace the index.
delete(iFolderEntryIndex);
iFolderEntryIndex=index;
// We have a new list, display from the top,
// if there is a top item.
iCurrentEntry=0;
LimitCurrentEntryVal();
// Update the path our entries exist within
// now all constructed.
iPath=aPath;
// While it may have been tempting to perform
// this immedately after the end of the loop above
// it is very important to match Push/Pops on
// the cleanup stack, so we simply defer to here.
CleanupStack::PopAndDestroy(dirList);
// Perform a self test to verify our internal
// structure is still self consistent.
__TEST_INVARIANT;
}
RArray compared to CArrayX classes
As we discussed in the Chapter Symbian OS essentials, there are two sets of collection classes in Symbian OS, the original CArrayX classes and the newer RArray classes.
Both types of collection class are relatively easy to use so, while there are some minor performance differences that we outline in the Chapter Refining your application, our application demonstrates the use of both classes, enabling you to choose your preferred set.
Creating the Set of TFolderEntrys
The BuildDirectoryListL() method contains the following code to create and store the set of TFolderEntrys:
// Create a new container to store the entries.
RArray<TFolderEntry>* folderEntryList=new(ELeave)RArray<TFolderEntry>(8);
// Not only do we need to ensure
// we delete the actual allocated Rarray.
CleanupDeletePushL(folderEntryList);
// But we also have to ensure we call Close()
// before the delete to release any resources.
CleanupClosePushL(*folderEntryList);
// Add a set of entries to our new container.
TFolderEntry entry;
const TInt count=dirList->Count();
for (TInt i=0;i<count;i++)
{
// Construct a new entry
entry.Construct((*dirList)[i]);
// Add to the new list of entries.
folderEntryList->AppendL(entry);
}
... remaining code removed for clarity.
The following points are worth noting:
- To cleanup the
RArrayfully we have to call theClose()method of the object AND delete the memory allocated to the actual object. TheCleanupClosePushL()templated function ensures theClose()method is called if a leave occurs. TheCleanupDeletePushL()function ensures the object is destroyed. The order presented above is important to ensure the object is closed before being deleted.
- We use the
AppendL()method. A common misuse ofRArrayclasses is to use theAppend()method and disregard the return value. TheCArrayXclasses don’t allow you to do this since they don’t supportAppend(). In all cases the method fails to add entries if it runs out of memory. Your application needs to do something about this. By usingAppendL()the method leaves when this condition occurs.
| Pro tip:
You should remember that the |
Building the Index
The index is created with the following code:
CArrayFixFlat<TInt>* CAppEngine::BuildIndexL(
RArray<TFolderEntry>* aList,
const TInt aCategory)
{
const TInt count=aList->Count();
TInt granularity=count;
if (!count)
granularity=1;
CArrayFixFlat<TInt>* index = new(ELeave)CArrayFixFlat<TInt>(granularity);
CleanupStack::PushL(index);
for (TInt i=0;i<count;i++)
{
if (aCategory==EAppCategoryAll || (*aList)[i].EntryCategory()==aCategory)
index->AppendL(i);
}
index->Compress();
CleanupStack::Pop(index);
return(index);
}
This code fragment demonstrates various aspects of using collection classes that you should consider using in your applications.
We calculate a granularity for the array. By making the granularity equal to the number of entries that we put in the array, we ensure that only a single memory allocation occurs. This means we are very efficient in terms of the number of memory allocations that occur, irrespective of the number of entries we are about to put into the array.
As an alternative to the above you could choose to use the default granularity, then use the SetReserveL() method to ensure the array has space for the number of entries you are about to add. As with setting the granularity, using SetReserveL() ensures that only a single memory allocation occurs.
In our example, for the cases where only a subset of entries is added to the index due to the category filtering, we have some spare capacity within an array. Good application memory management requires we free up this spare capacity since we are not going to use it. We achieve this by using the Compress() method. Even if we used the system default value for the granularity we should still use the Compress() method to release any spare capacity. The Compress() method will not cause a re-allocation of the underlying memory; it simply returns unused memory to the heap allocator.
In our example code, the worst case scenario is that we have one memory allocation irrespective of the number of elements we put in our index. The typical default granularity is eight. This means that for every eight entries added to the index, a memory re-allocation and content copy occurs. Memory re-allocation is an expensive operation.
Sorting the Index
If we had chosen to use an RArray to store our index we would need to use a TLinearOrder function to perform our sort. An example of this is presented in the Commands1 application.
Since we are using a CArrayX class we need to use a TKey derived class to perform the sort. As with many Symbian OS objects, rather than have either blank or pure virtual functionality, some default functionality is implemented. In some cases this functionality may be sufficient, in others it may not. The TKeyArrayFix class is a classic example of this approach. The default implementation provides a significant set of functionality; however none is sufficient for our use. The standard C++ approach to extending functionality is to override virtual functions. In this case we override the comparison function using the following object definition:
class TKeyEntryList : public TKeyArrayFix
{
public:
TKeyEntryList(RArray<TFolderEntry>*aFolderEntryList, const TInt aSortType, const TInt aDescending);
TInt Compare(TInt aLeft,TInt aRight) const;
protected:
RArray<TFolderEntry>* iFolderEntryList;
TInt iSortOrder;
};
The implementation of the Compare() function is:
TInt TKeyEntryList::Compare(TInt aLeft,TInt aRight) const
{
TFolderEntry& leftEntry=(*iFolderEntryList)[*static_cast<TInt*>(At(aLeft))];
CONVERT_POINTER_TYPES conv;
conv.thisIsAPointerToTheObject=At(aRight);
TFolderEntry& rightEntry=(*iFolderEntryList)[(*conv.whichWeConvertToOneOfThese)];
// Left item comes before right item.
TInt ret=(-1);
switch (iCmpType)
{
// Sort by file size, if they are
// the same order by name.
case EFolderEntrySortBySize:
ret=leftEntry.EntrySize() - rightEntry.EntrySize();
if (ret==0)
ret=leftEntry.EntryName().CompareF(rightEntry.EntryName());
break;
// Sort by name.
case EFolderEntrySortByName:
ret=leftEntry.EntryName().CompareF(rightEntry.EntryName());
break;
// Sort by last modified date,
// if they are the same order by name.
case EFolderEntrySortByModified:
if (leftEntry.EntryModified()>rightEntry.EntryModified())
ret=1;
else if (leftEntry.EntryModified()== rightEntry.EntryModified())
ret=leftEntry.EntryName().CompareF(rightEntry.EntryName());
break;
default:
break;
}
if (iSortOrder)
ret=(-ret); // Simply reverse the result
// to get descending order sorts.
return(ret);
}
Since CArrayX classes can be used to store objects of practically any type and due to the original implementation, the At() method simply returns a TAny*, that is, a pointer to one of the elements in our CArrayX. Since our CArrayX is an array of integers the TAny* needs to be converted into a TInt*. There are numerous ways in which we can convert pointer types. This application demonstrates two options, a static_cast to convert the left index and a union to convert the right index.
typedef union
{
TAny* thisIsAPointerToTheObject;
TInt *whichWeConvertToOneOfThese;
} CONVERT_POINTER_TYPES;
For our application we read the index values as stored in the CArrayX and use those to get references to the underlying TFolderEntrys. This particular task is difficult to achieve with a TLinearOrder as there is no obvious way to gain addressability to data stored outside the RArray being sorted.
Once we have determined which two entries are to be compared we can perform the comparison function, returning a negative value if the left entry comes before the right entry, positive is the left entry comes after the right entry and zero if the entries are identical. Our application has chosen to implement some second order key information. If we are sorting by size or modification date then matching entries are also sorted by name.
To actually cause the CArrayX to be sorted we do this:
TKeyEntryList key(folderEntryList,iSortType,iSortOrder); index->Sort(key);
The List View
The list view displays the entries belonging to the index, in the order the index specifies. Rather than the UI code needing to know this, or how to translate between the index and an actual entry, the engine presents a couple of functions for the UI to call:
TInt EntryCount() const; const TFolderEntry& Entry(const TInt aIndex) const;
Constructing the list box reduces to:
CQikListBox* listbox = LocateControlByUniqueHandle<CQikListBox>(EListViewListId);
// Get the listbox model.
MQikListBoxModel& model(listbox->Model());
// Remove any existing entries.
model. RemoveAllDataL();
model.ModelBeginUpdateLC();
const TInt count=iEngine->EntryCount();
for (TInt i=0;i<count;i++)
{
MQikListBoxData* lbData = model.NewDataL(MQikListBoxModel::EDataNormal);
CleanupClosePushL(*lbData);
lbData->AddTextL(iEngine->Entry(i).EntryName(), EQikListBoxSlotText1);
lbData->SetItemId(i);
CleanupStack::PopAndDestroy(lbData);
}
model.ModelEndUpdateL();
}
This should be familiar code from previous examples.
Sorting in the UI
As with the Commands1 application, we have a cascade menu option presenting the sort options available to the user. We override the DynInitOrDeleteCommandL() method of the list view to ensure the UI presentation of the sort type radio buttons and sort order check box match the internal values.
CQikCommand* CListView::DynInitOrDeleteCommandL(
CQikCommand* aCommand,
const CCoeControl& aControlAddingCommands)
{
TBool val=EFalse;
switch (aCommand->Id())
{
// Determine which of the radio buttons
// should be checked.
case EAppCmdSortByName:
if (iEngine->SortType()==EFolderEntrySortByName)
val=ETrue;
aCommand->SetChecked(val);
break;
case EAppCmdSortBySize:
if (iEngine->SortType()==EFolderEntrySortBySize)
val=ETrue;
aCommand->SetChecked(val);
break;
case EAppCmdSortByType:
if (iEngine->SortType()==EFolderEntrySortByType)
val=ETrue;
aCommand->SetChecked(val);
break;
case EAppCmdSortByDate:
if (iEngine->SortType()==EFolderEntrySortByModified)
val=ETrue;
aCommand->SetChecked(val);
break;
// Determine whether the single check box
// type menu option should be checked.
case EAppCmdSortOrder:
if (iEngine->SortOrder()==ESortOrderAscending)
val=ETrue;
aCommand->SetChecked(val);
break;
default:
break;
}
return(aCommand);
}
The user request to sort the list causes a CQikCommand object to be delivered to the HandleCommandL() method in which we request the engine to sort the index:
void CListView::HandleCommandL(CQikCommand& aCommand)
{
switch (aCommand.Id())
{
case EAppCmdSortByName:
SortListL(EFolderEntrySortByName,iEngine->SortOrder(), EAppCmdSortByName);
break;
case EAppCmdSortBySize:
SortListL(EFolderEntrySortBySize,iEngine->SortOrder(), EAppCmdSortBySize);
break;
case EAppCmdSortByDate:
SortListL(EFolderEntrySortByModified,
iEngine->SortOrder(),EAppCmdSortByDate);
break;
case EAppCmdSortByType:
SortListL(EFolderEntrySortByType,iEngine->SortOrder(), EAppCmdSortByType);
break;
case EAppCmdSortOrder: // Swap between ascending and descending.
SortListL(iEngine->SortType(),
(TFolderEntrySortOrder)(ESortOrderAscending +
ESortOrderDescending-iEngine->SortOrder()), EAppCmdSortOrder);
break;
default: // e.g. the back button...
CQikViewBase::HandleCommandL(aCommand);
break;
}
}
Once sorted, we have to rebuild the list box. In our application we have chosen to rebuild the list box from scratch as there is no easy way of re-ordering the list box elements. Since this operation can fail we need to implement some rollback to ensure that either the operation fully succeeds or the application returns to the state it was previously in.
void CListView::SortListL(
const TFolderEntrySortType aType,
const TFolderEntrySortOrder aOrder,
const TInt aCmdId)
{
// Record current settings incase we need to roll back.
TFolderEntrySortType oldType=iEngine->SortType();
TFolderEntrySortOrder oldOrder=iEngine->SortOrder();
// Update the list.
SetCurrentEntry();
iEngine->SortEntries(aType,aOrder);
// Attempt to update the listbox display,
// track the current entry...
TRAPD(err,
UpdateListBoxL();
LocateControlByUniqueHandle<CQikListBox>
EListViewListId)->SetCurrentItemIndexL(
iEngine->CurrentEntryIndex(),ETrue,EDrawNow);
);
// If the listbox display fails, rollback
// to previous sort order.
if (err!=KErrNone)
{
iEngine->SortEntries(oldType,oldOrder);
CQikCommandManager& cm=CQikCommandManager::Static();
// Restore the Radio button state within the UI.
if (oldType!=aType)
{
TInt commandId=EAppCmdSortByName;
if (oldType==EFolderEntrySortBySize)
commandId=EAppCmdSortBySize;
else if (oldType==EFolderEntrySortByModified)
commandId=EAppCmdSortByDate;
else if (oldType==EFolderEntrySortByType)
commandId=EAppCmdSortByType;
if (commandId!=aCmdId)
{ // Swap back the radio button state
cm.SetChecked(*this,aCmdId,EFalse);
cm.SetChecked(*this,commandId,ETrue);
}
}
// Restore the Ascending check box within the UI.
if (oldOrder!=aOrder)
{
TBool val=EFalse;
if (oldOrder==ESortOrderAscending)
val=ETrue;
cm.SetChecked(*this,EAppCmdSortOrder,val);
}
User::Leave(err);
}
}
The ability of the engine to sort entries without leaving is important here. To roll back fully we need to put the application back to the state it was originally in before the sort operation started. The ability to sort the entries back to their previous state without leaving is part of the required rollback.
Tracking the Current Entry
In small screen devices, such as a mobile phone, the usability of an application is of paramount importance. When a list is sorted, the lines of content displayed in the list are changed. Rather than simply let the focus remain on the same line, hence different content, it is often preferable to either cause the focus to move with the content or perhaps to reset the focus to the top of the list. In most cases, causing the focus to move with the content provides a superior user experience since contextual information is maintained. The user can immediately see where the selected entry fits within the newly sorted list without having to search for it. After all, a primary reason to sort entries is to present contextual information about entries.
Our engine tracks the current entry in the sort method:
void CAppEngine::SortEntries(
const TfolderEntrySortType aSortType,
const TFolderEntrySortOrder aOrder)
{
TInt whichEntry=iFolderEntryIndex->At(iCurrentEntry);
TKeyEntryList key(iFolderEntryList,aSortType,aOrder);
iFolderEntryIndex->Sort(key);
iSortType=aSortType;
iSortOrder=aOrder;
// Track the ‘current’ entry.
const TInt count=EntryCount();
for (TInt i=0;i<count;i++)
{
if (iFolderEntryIndex->At(i)==whichEntry)
{
iCurrentEntry=i;
break;
}
}
}
The Detail View
In a list and detail model, the detail view is used to view and edit individual entries. Our detail view has a set of controls which allow the user to change the name, assign a category and update the last modified date of an entry. Since we have chosen not to make the detail view public to external applications, we do not need to support DNL links to the detail view. We use the simplified approach of informing the engine which entry we wish to display before switching to the detail view:
void CListView::HandleListBoxEventL(
CQikListBox* aListBox,
TQikListBoxEvent aEventType,
TInt aItemIndex,
TInt aSlotId)
{
switch (aEventType)
{
case EEventItemConfirmed:
case EEventItemTapped:
iEngine->SetCurrentEntry(aItemIndex);
iQikAppUi.ActivateViewL(KViewIdDetailsView);
break;
default:
break;
}
}
The ViewActivatedL() method picks up the current entry and configures the component controls with data associated with that entry.
void CDetailsView::ViewActivatedL(
const TVwsViewId& aPrevViewId,
const Tuid aCustomMessageId,
const TDesC8& aCustomMessage)
{
const TFolderEntry& entry=iEngine->CurrentEntry();
CEikEdwin* edwin = LocateControlByUniqueHandle<CEikEdwin>(EAppEdwin1);
edwin->SetTextL(&entry.EntryName());
CEikChoiceList* cl = LocateControlByUniqueHandle<CEikChoiceList>(EAppChoiceList1);
cl->SetArrayL(this);
cl->SetArrayExternalOwnership(ETrue);
TInt id=entry.EntryCategory();
TInt count=iEngine->CategoryListCount();
for (TInt i=1;i<count;i++)
{
if (iEngine->CategoryListAt(i).iCategoryId==id)
{
cl->SetCurrentItem(i-1);
break;
}
}
CQikTimeAndDateEditor* dt = LocateControlByUniqueHandle<CQikTimeAndDateEditor>(EAppDateTime1);
dt->SetTimeL(entry.EntryModified());
}
To support categories we have to perform two tasks:
- provide a list of the categories the user can choose between
- set up which category the current entry belongs to.
To provide the list of categories the choice list needs an object to implement the MDesCArray interface. The CDetailsView object can provide that interface, and does so with the following code:
TInt CDetailsView::MdcaCount() const
{
return(iEngine->CategoryListCount()-1);
}
TPtrC CDetailsView::MdcaPoint(TInt aIndex) const
{
return(iEngine->CategoryListAt(aIndex+1).iCategoryName);
}
You should have observed a small translation, by a factor of one, occurring in most of the code associated with categories. In general you have the All, Unfiled and a set of application-specific categories. No entries should be set to belong to the All category, they should belong to either the Unfiled or an application-specific category. The All category is simply a mechanism for the user to view items irrespective of which category they belong to. When we present the list of categories an entry can belong to we should remove the All category. It is no accident that the All category is first category entry created in the engine. With this arrangement we can simply adjust counts and indices with the above code.
Updating Entries
One objective of the detail view is to enable a user to update an entry. As with the MultiView4 application presented previously, if the user chooses to move back to the list view, we should check whether any of the details have been modified. Our application always prompts the user to save changes. You may prefer a Cancel and Done approach where Cancel prompts prior to abandoning any changes, while Done saves changes without any prompt.
Unlike the MultiView4 application, changes to entries in this application have a more profound effect on the displayed list. For example:
- If you change the category to which the entry belongs and the list box is displaying a specific category (as opposed to the All category), the entry should no longer be listed since it no longer belongs to the category being displayed.
- If you change the entry name, it is unlikely the entry should remain in the same position within the sorted list box entries
In the MultiView4 application we chose to ActivatePreviousViewL(ESave) to return to the previous view, calling the CAppSpecificDetailsView::SaveL() method when required.
ActivatePreviousViewL(ESave)calls the SaveL() method asynchronously. In particular it calls SaveL() AFTER it has called the CListView::ViewActivatedL() method. While appropriate for the MultiView4 example, this is not satisfactory for our application. We need to update the list before we attempt to display it.
Since our application needs to update any changes before we switch back to the list view we should use the SaveThenDnlToL(ParentView()) method. This calls the SaveL() method synchronously before the framework attempts to switch views:
void CDetailsView::HandleCommandL(CQikCommand& aCommand)
// Handle the commands coming in from the controls
// that can deliver cmds…
{
switch (aCommand.Id())
{
case EAppCmdAbout:
iQikAppUi.HandleCommandL(EAppCmdAbout);
break;
case EQikCmdGoBack:
if (DetailsHaveChanged())
{
CQikSaveChangesDialog::TexitOption
ret=CQikSaveChangesDialog::RunDlgLD();
if (ret==CQikSaveChangesDialog::EShut)
break;
if (ret==CQikSaveChangesDialog::ESave)
{
SaveThenDnlToL(ParentView());
break;
}
}
default: // e.g. the back button...
CQikViewBase::HandleCommandL(aCommand);
break;
}
}
Updating Entries in the Engine
The engine is responsible for actually updating a TFolderEntry. It has several tasks to perform:
- If the name has changed, the underlying filename also needs to be changed as well as the
TFolderEntrycontent. - If the last modified date has changed, the underlying file date should also be updated.
- If the category has changed, we may need to adjust the set of entries being displayed by the list box.
- If a change affects the current list box sort order, the entries should be sorted into their new order.
void CAppEngine::UpdateCurrentEntryL(
const TFolderEntry& aUpdate)
{
TFolderEntry& curEntry=EntryListAt(
iFolderEntryIndex->At(iCurrentEntry));
TParse srcName;
srcName.Set(curEntry.EntryName(),&iPath,NULL);
TBool sortReqd=EFalse;
if (curEntry.EntryName()!=aUpdate.EntryName())
{ // Names being changed.
TParse destName;
destName.Set(aUpdate.EntryName(),&iPath,NULL);
User::LeaveIfError(iFs.Rename(srcName.FullName(), destName.FullName()));
curEntry.UpdateName(aUpdate.EntryName());
sortReqd=ETrue;
}
if (curEntry.EntryModified()!=aUpdate.EntryModified())
{
User::LeaveIfError(iFs.SetModified(srcName.FullName(), aUpdate.EntryModified()));
curEntry.UpdateModified(aUpdate.EntryModified());
if (iSortType==EFolderEntrySortByModified)
sortReqd=ETrue;
}
if (curEntry.EntryCategory()!=aUpdate.EntryCategory())
{
TInt oldCategory=curEntry.EntryCategory();
curEntry.UpdateCategory(aUpdate.EntryCategory());
if (iCurrentCategory==oldCategory)
{
iFolderEntryIndex->Delete(iCurrentEntry);
LimitCurrentEntryVal();
}
}
if (sortReqd)
SortEntries(iSortType,iSortOrder);
}
Our sort operation tracks the current entry. This is useful because if the list needs to be sorted due to an entry being updated, we automatically handle any positional change of the current entry. When we return to the list view, the entry whose details we were manipulating remains the current entry, assuming it is not removed from the list due to a change in category. Maintaining this contextual information enhances usability.
Note that the engine code above makes an assumption regarding the UI. In particular, it assumes that it is not possible to move an entry into a category the UI is currently displaying, only out of the displayed category. In our application this is true since we only have a single list of items and no mechanism to change the details of items that are not displayed in the list. Other applications may have more than one list or an alternate mechanism to set entry categories. In this case, additional functionality may be required to ensure any change of category causes the entry to appear in all lists to which it belongs.
Deleting Entries
Our application supports the ability to delete entries. Currently we do not support any undo functionality, such as a Recycle Bin, so it is important to ensure the user really wants to perform the delete operation. To delete an entry we need to:
- remove the entry from disk
- remove the entry from our
TFolderEntrylist - update the index
- update the display.
The engine performs the first three tasks:
void CAppEngine::DeleteCurrentEntryL()
{
TFileName name;
EntryFullName(name);
User::LeaveIfError(iFs.Delete(name));
TInt index=iFolderEntryIndex->At(iCurrentEntry);
iFolderEntryList->Remove(index);
iFolderEntryIndex->Delete(iCurrentEntry);
const TInt count=EntryCount();
for (TInt i=0;i<count;i++)
{
TInt val=iFolderEntryIndex->At(i);
if (val>=index)
(*iFolderEntryIndex)[i]=val-1;
}
LimitCurrentEntryVal();
}
Rather than create a whole new index, filtered and sorted according to the user’s current preferences, we manipulate the existing index. This has a couple of advantages: firstly it is not possible to run out of memory attempting to build the new index and secondly it is considerably faster.
The UI performs the remaining tasks:
void CListView::DeleteCurrentEntryL()
{
SetCurrentEntry();
const TFolderEntry& entry=iEngine->CurrentEntry();
TBuf<256>bb;
iEikonEnv->Format256(bb,R_STR_ABOUT_TO_DELETE, &entry.EntryName());
TBuf<64>bb2;
iEikonEnv->ReadResourceL(bb2,R_STR_ATTENTION);
if (iEikonEnv->QueryWinL(bb2,bb))
{
iEikonEnv->Format256(bb,R_STR_DELETED, &entry.EntryName());
iEngine->DeleteCurrentEntryL();
CQikListBox* listbox =
LocateControlByUniqueHandle<CQikListBox>(EListViewListId);
listbox->RemoveItemL(listbox->CurrentItemIndex());
TInt i=iEngine->CurrentEntryIndex();
if (i>=0)
listbox->SetCurrentItemIndexL(i,ETrue,EDrawNow);
iEikonEnv->InfoMsg(bb);
UpdateCommandAvailability();
}
}
As a general rule, providing some positive feedback that an operation has completed is almost as important as requesting confirmation that an operation should be performed or presenting error information. Such positive feedback should be unobtrusive and preferably not require any user interaction. The use of iEikonEnv->InfoMsg() to display an informational message is ideal for this task as it automatically disappears after a couple of seconds.
Command Availability
Actions are usually performed on content, for example you delete an entry. If no entries exist then many commands have little meaning and most applications reach a boundary condition.
For example, in our CListView::DeleteCurrentEntryL() method the software assumes that it can always return a TFolderEntry&. If no entries exist, the current entry index is -1. Attempting to obtain the TFolderEntry& results in a CBase-21 panic, indicating that the array index is out of range. Rather than relying on users not to attempt such operations, you need to be proactive and write code to ensure the application does not panic.
In general there are two approaches:
- Add some code to verify that the operation can be performed without causing a panic; for example, make sure there is at least one entry that can be deleted.
- Remove the option from the UI, therefore preventing the user from choosing an option that cannot be performed. Our application has chosen this approach:
void CListView::UpdateCommandAvailability()
{
CQikCommandManager& cm=CQikCommandManager::Static();
TBool avail=EFalse;
if (iEngine->EntryCount()>0)
avail=ETrue; // Some entries exist in the current filtered list.
cm.SetAvailable(*this,EAppCmdOpen,avail);
cm.SetAvailable(*this,EAppCmdDelete,avail);
cm.SetAvailable(*this,EAppCmdDelete2,avail);
cm.SetAvailable(*this,EAppCmdSendAs,avail);
cm.SetAvailable(*this,EAppCmdSortCascade,avail);
}
Don’t forget that not all commands arrive by the user choosing a menu option. In our application we have delete in the menu and delete assigned to the Cancel key through the command type EQikCommandTypeDelete. Both these delete commands need to be marked as unavailable.
| Pro tip:
If you define two identical commands to get one in the menu and one on a hardware key, make sure you don’t get both commands in the more menu if the softkey algorithm changes. Make one command of type |
Category Management
Categories and entries are very closely linked. With the exception of changing the name of a category, any manipulation has an effect on either the list of entries displayed or the entries themselves.
Changing Categories
If a user chooses to display the set of entries belonging to a specific category, our application has to generate a new sorted index on to those entries.
void CAppEngine::ChangeCategoryL(const TInt aHandle)
{
CArrayFixFlat<TInt>* index=BuildIndexL(iFolderEntryList,aHandle);
TKeyEntryList key(iFolderEntryList,iSortType,iSortOrder);
index->Sort(key);
iCurrentCategory=aHandle;
delete(iFolderEntryIndex);
iFolderEntryIndex=index;
iCurrentEntry=0;
LimitCurrentEntryVal();
}
The UI is responsible for updating the list box to display the set of entries that belong to the chosen category:
void CListView::HandleCommandL(CQikCommand& aCommand)
{
if (aCommand.Type()==EQikCommandTypeCategory)
{
if (aCommand.Id()==EAppCmdEditCategories)
CQikEditCategoriesDialog::RunDlgLD(CategoryModel(), this);
else
{
iEngine->ChangeCategoryL(aCommand.CategoryHandle());
SelectCategoryL(aCommand.CategoryHandle());
UpdateListBoxL();
UpdateCommandAvailability();
}
}
}
Note that if we choose a category containing zero entries, we need to ensure the user is not able to choose commands that could cause our application to panic. While subtly different from the case where no entries exist at all, by encapsulating entry count and referencing in the engine, the UI does not need to know about this difference and the same logic can be used to determine command availability.
Deleting Categories
In our application, we allow the user to delete categories even if there are entries associated with that category. When this happens, we must take one of the following methods to tidy up afterwards:
- delete all the entries associated with the category
- accept that entries can belong to non-existent categories and therefore only appear within the All category until the user manually updates the entry.
- adjust any entries that belong to a category that is about to be deleted such that they belong to a different category. Our application implements this option by moving entries to the Unfiled category.
Our application also allows the user to delete the category that the list view is currently displaying. In this case something reasonable has to be done to update the list view, such as display a different category of entries. Reverting to the All entries category is a safe option since this category cannot be deleted. We have implemented this option in our application:
TInt CAppEngine::DeleteCategoryL(const TInt aHandle)
{
TInt i;
TInt ret=0;
if (aHandle==iCurrentCategory)
{
CArrayFixFlat<TInt>* index = BuildIndexL(iFolderEntryList,EAppCategoryAll);
TkeyEntryList key(iFolderEntryList,iSortType, iSortOrder);
index->Sort(key);
delete(iFolderEntryIndex);
iFolderEntryIndex=index;
iCurrentEntry=0;
LimitCurrentEntryVal();
iCurrentCategory=EAppCategoryAll;
ret=1;
}
TInt count=EntryListCount();
iFolderEntryIndex->SetReserveL(count);
TKeyEntryList key(iFolderEntryList,iSortType,iSortOrder);
for (i=0;i<count;i++)
{
TFolderEntry& entry=EntryListAt(i);
if (entry.EntryCategory()==aHandle)
{
entry.UpdateCategory(EAppCategoryUnfiled);
if (iCurrentCategory==EAppCategoryUnfiled)
{
if (iFolderEntryIndex->InsertIsqAllowDuplicatesL(i,key)<=iCurrentEntry)
iCurrentEntry++;
ret=(-1);
}
}
}
iFolderEntryIndex->Compress();
count=CategoryListCount();
for (i=0;i<count;i++)
{
TAppCategoryEntry& category=CategoryListAt(i);
if (category.iCategoryId==aHandle)
{
iCategoryList->Remove(i);
break;
}
}
return(ret);
}
As discussed, when we update entries that belonged to the category being deleted we move them to the Unfiled category. If the UI happens to be displaying the Unfiled category these entries should become visible to the user. To make the entries visible we have to expand the index. Any expansion can fail due to becoming out of memory. In our application we adopt an all or nothing approach by ensuring that the index is expanded so that it can contain all the required entries by using iFolderEntryIndex->SetReserveL(count). This also provides a minor performance benefit because the index is only re-allocated a maximum of one time, in the SetReserveL() method, to contain any extra items.
The user interface presents entries to a user in the currently chosen sort order. The presentation order is dictated by the content of the index. When we add entries to the index we should maintain the currently chosen sort order. This can be achieved by either appending entries to the end then sorting the full list, or inserting the entries in the correct place. Our implements the latter, and so uses the InsertIsqAllowDuplicatesL() method. As with an RArray the CArrayX reuses the entry comparison object to determine the insert position. The InsertIsqAllowDuplicatesL() method has a further useful property; it returns the position in the index the new item is inserted. We use this information to track the currently selected item. When a UI returns from the category management dialogs the contextual information within the list box is preserved.
| Pro tip:
Due to the way views and controls work in UIQ 3, if you leave a choice list control in the detail view referencing a category other than the All or Unfiled categories, and some categories are deleted, then even though the detail view is deactivated your application panics with |
void CDetailsView::ViewDeactivated()
{
LocateControlByUniqueHandle<CEikChoiceList>(EAppChoiceList1)->SetCurrentItem(0);
}
Send As
In some applications, including ours, the ability to transfer content from one device to another is a useful feature. The UIQ 3 framework contains a useful set of functionality called Send as to perform this task.
The Send as framework code has a number of properties:
- it presents a standard interface to the user
- it is easy to use
- it supports a range of transports
- no Symbian OS platform security capabilities are required to successfully use the Send as framework code.
This code is all you need to support the sending of files via the transports which accept attachments.
void CListView::SendFileAsL()
// Send the file, via a user chosen transport.
{
CQikSendAsLogic* sendAs=CQikSendAsLogic::NewL();
CleanupStack::PushL(sendAs);
TFileName fullName;
iEngine->EntryFullName(fullName);
sendAs->AddAttachmentL(fullName);
CleanupStack::Pop(sendAs);
CQikSendAsDialog::RunDlgLD(sendAs, KUidMtmQuerySupportAttachments);
}
On a real mobile phone this typically means infrared, Bluetooth and MMS transports.
You should note that ownership of the SendAs object is taken immediately by the RunDlgLD() method and in particular before that method can leave. Therefore it is important to remove the SendAs object from the cleanup stack before calling RunDlgLD(). This is not typical Symbian OS behavior.
This is a minimal implementation of sending files. The UIQ 3 SDK documentation has further information about the CQikSendAsLogic and CQikSendAsDialog objects which you can review if the above is not sufficient for your requirements. For example you may wish to add some Subject or Body text, particularly if the file is being transferred via MMS.
Registration Using a Phone IMEI
A common distribution mechanism is to provide free downloads of an application in which the functionality is restricted. The user can purchase a full licence, at which time an unlock key or registration code is supplied. The user can enter this code into the downloaded version, converting it into a full version of the application. It is not necessary to uninstall the free version or install a separate full version.
Many distribution sites support the so called Dynamic Registration model, where the registration code is based on some unique piece of information associated with a mobile phone, in the case of GSM and UMTS phones this is usually the international mobile equipment identity number, known for short as the IMEI.
Obtaining the IMEI of the phone has to be performed by calling an asynchronous function supplied by the CTelephony class.
An active object class must be defined to submit the asynchronous request and handle the completion event, but this is relatively simple, and the entire object definition and functionality is:
class CGetPhoneIMEI : public CActive
{
protected:
void RunL();
void DoCancel();
public:
~CGetPhoneIMEI();
CGetPhoneIMEI(CAppSpecificUi* aAppUi);
void ConstructL();
protected:
CAppSpecificUi* iAppUi;
CTelephony::TPhoneIdV1Pckg iBuf;
CTelephony::TPhoneIdV1 iData;
CTelephony* iTelephony;
};
CGetPhoneIMEI::~CGetPhoneIMEI()
{
Cancel();
delete(iTelephony);
}
CGetPhoneIMEI::CGetPhoneIMEI(CAppSpecificUi* aAppUi) :
CActive(CActive::EPriorityHigh), iAppUi(aAppUi),iBuf(iData)
{
CActiveScheduler::Add(this);
}
void CGetPhoneIMEI::ConstructL()
// Start the request to obtain the IMEI.
{
iTelephony=CTelephony::NewL();
iTelephony->GetPhoneId(iStatus,iBuf);
SetActive();
}
void CGetPhoneIMEI::RunL()
{
iAppUi->SetIMEI(iData.iSerialNumber);
delete(this);
}
void CGetPhoneIMEI::DoCancel()
{
iTelephony->CancelAsync(CTelephony::EGetPhoneIdCancel);
}
For more information about Symbian OS active objects please consult the Chapter Symbian OS essentials.
The code above demonstrates several points:
- We add the active object at a priority of
CActive::EPriorityHigh. This does not mean that the request takes precedence over any other system request; it means that as soon as the information is delivered one of the first active objects to be checked is this one. Obtaining the phone IMEI is a fast operation that is only asynchronous by virtue of communicating with a server therefore this active object gets to run before most of our other active objects. - The
RunL()method executes adelete(this); once we have obtained the IMEI there is no further use for the active object. Deleting yourself within theRunL()is perfectly safe since no further object property access is required and the active scheduler is especially designed to ensure it can handle objects removing themselves from the active object queue within theRunL().
Using the CGetPhoneImei is just as easy:
#ifdef __WINS__
iPhoneImei=_L("98-765432-123456-7");
#else
CGetPhoneIMEI* imei=new(ELeave)CGetPhoneIMEI(this);
CleanupStack::PushL(imei);
imei->ConstructL();
CleanupStack::Pop(imei);
#endif
Note that the IMEI is hard-coded for the WINS build because the telephony components are not fully supported by CTelephony for the Symbian OS Windows emulator.
Once the ConstructL() method completes successfully, or more particularly the code iTelephony == CTelephony::NewL() within the ContructL() method completes successfully, the active object is set up and its RunL() method is called on completion of the asynchronous event. The CGetPhoneImei takes ownership of itself – it deletes itself when the RunL() gets called. We do not need to take ownership of the CGetPhoneImei anywhere else within the system.
Figure 10 Registration dialog box
Once the IMEI has been retrieved, the CGetPhoneImei object calls the iAppUi->SetIMEI() method which simply stores the IMEI in the iPhoneImei property. While there is still a small gap between the application starting up and the IMEI being available, in most situations an application can be constructed such that this gap is irrelevant.
Prompting for a Registration Code
Our application contains a Register menu option which presents a dialog to the user. Within the dialog we display the currently obtained IMEI and request the user enter the registration code.
| Pro tip:
Displaying a mobile phone’s IMEI is very useful for support purposes. A common support issue with Dynamic Registration is that it relies on the customer to correctly enter their IMEI on a website at the time of purchase. Naturally a percentage of customers fail to do this. Having an easy way for customers to obtain their IMEI – by reading it from your Register dialog compared to entering |
Obtaining the IMEI and CAPABILTIES
For some early UIQ 3 phones, it is necessary to have two capabilities in order to obtain the IMEI. This is because these products used a version of Symbian OS v9 that had an internal dependency within the telephony software which required applications to have the ReadDeviceData and NetworkServices capabilities. This dependency has now been removed and IMEI can be freely retrieved on most UIQ 3 phones.
If you have a requirement to support the early phones you need to add the ReadDeviceData and NetworkServices capabilities to your application. If, like ours, your application is to be Symbian Signed, then adding these capabilities has little effect on the testing process.
Since ReadDeviceData is a System capability, you must obtain the capability via Symbian Signed if you need to retrieve IMEI on the very early UIQ 3 phones.
Loading and Saving
In general applications have two different types of data. The first type is the actual information being presented to the user. The second type is how the information is presented. For example, in a Contacts or Agenda type application the first type of data would be the contact records or agenda entries. The second type of data would be which zoom level, category or sort order that has been chosen to present the data.
When applications close most users expect both types of data to be preserved. When going back to the application, they expect it to appear exactly as it last was, almost as though the application had not been closed.
The UIQ 3 view framework provides substantial support for record based applications through the SaveL() method of the CQikViewBase class. As users move away from a view the application can choose how to handle any updates that may have occurred. In our application, the CDetailsView updates the information stored on disk (such as renaming a file) as well as any internal representation information. Database type applications would normally update the database record at this time.
While the underlying data is normally updated when a view change occurs, it is unusual for the presentation information to be updated as well. In general presentation data is only stored when applications are closed.
Closing Applications
The current UIQ 3 style guidelines recommend that applications do not present a Close or Exit menu option to the user. On some UIQ 3 mobile phones a task manager is available which enables users to close applications. On all UIQ 3 mobile phones, if memory resources become low then the system requests applications to shut down, releasing resources.
In both cases your application is sent an EEikCmdExit command. To facilitate testing of any functionality associated with this command it is common practice to add an EQikCmdFlagDebugOnly type command to the application:
QIK_COMMAND
{
id = EEikCmdExit;
type = EQikCommandTypeScreen;
stateFlags = EQikCmdFlagDebugOnly;
groupId = EAppCmdMiscGroup;
priority = EAppCmdLastPriority;
text = "Exit";
}
| Pro tip:
Your application is free to vary from the UIQ 3 recommended guidelines if you feel this is appropriate to your application. Games in particular often have a Close menu option of some description, especially those that choose to run full screen. It is harder to navigate to the task manager from a fullscreen application; therefore a Close option can be useful. Your application will not fail Symbian Signed if you include a Close menu option. |
Where to Store Presentation Information
If your application does not store presentation information along with the primary data, an alternate location for presentation information is required.
Symbian OS supports a wide range of file services. You can choose from basic file services, where your application reads and writes information directly to the file in a format of your choice, through to abstract database record services.
One set of file services provided is called stream stores. Your application serializes its internal object network to an external stream to save information, and de-serializes that external stream into the internal object network to load information.
A detailed discussion about stream stores and their variations is beyond the scope of this book. The UIQ v3 SDK documentation contains a significant amount of information on this topic. Additional information regarding the structure and usage of streams and stores can be found in the Symbian OS C++ for Mobile Phones book published by Symbian Press.
For the purposes of our application, a stream store is quite simply a file that contains a binary stream of data which can be loaded and saved conveniently.
ExternalizeL()
In an object oriented environment, encapsulation is a common metaphor. When applied to saving content to a stream store, an object encapsulates its save functionality within an externalization method. The implementation of the ExternalizeL() method for the TAppCategoryEntry object is:
void TAppCategoryEntry::ExternalizeL(
RWriteStream& aStream) const
{
aStream.WriteUint8L(KAppCategoryEntryStreamVersion);
// iCategoryId.
aStream.WriteInt32L(iCategoryId);
// iCategoryName.
aStream.WriteUint8L(iCategoryName.Length());
aStream.WriteL((TText*)(iCategoryName.Ptr()), iCategoryName.Length());
}
Here we are storing a single byte for some version information, four bytes for the category ID, a single byte for the length of a text string and a number of two byte entities, one for each character in the text string.
It is up to you to ensure that the number of bytes being stored is sufficient to represent the range of values an item can contain. For example, the version number value is extremely unlikely to ever exceed 10. The value 10 can be represented in a single byte; therefore we only store a single byte.
| Pro tip:
The number of bytes stored to represent a value is a compromise between minimizing the required disk storage compared to ease of use and maintainability. We could quite easily store four bytes for the version number. Doing so means we store three bytes which is always 0. For every category we have we would store three bytes of redundant information. In our application we have a very small set of categories so this is not significant. Examine the needs of your application and store the number of bytes that you need and no more. |
Unlike traditional raw file based loading and saving, by using stream stores we don’t need to worry about ensuring any underlying buffering is sufficient, whether entities cross buffer boundaries, the endian-ness in which data is stored, exactly where data is being placed or indeed if the underlying data is being compressed, encrypted or both. The stream store takes care of all those details. Similarly when we come to load the data all these issues are handled by the framework code. This makes the application code significantly easier to write and maintain.
Why is the method name ExternalizeL()?
In our application we have a set of categories owned by the engine object and stored within an RArray. A single category entry is saved using the above code. To save the entire set of category entries, the engine needs to call the ExternalizeL() method on each of the TAppCategoryEntrys.
The << operator
Symbian OS contains a template for the << operator. This template expects to see a method name of ExternalizeL() taking a single RWriteStream& parameter belonging to the object to which the << operator is applied. If we choose to call our object saving function ExternalizeL(RWriteStream&) then we can choose whether to use the << operator or call the method directly. In our application we have named our function such that we can use the << operator. The engine can save the set of categories with the following code:
TInt count=iCategoryList->Count(); aStream.WriteInt16L(count); for (i=0;i<count;i++) aStream<<((*iCategoryList)[i]);
The identical functionality could have been performed with:
TInt count=iCategoryList->Count(); aStream.WriteInt16L(count); for (i=0;i<count;i++) (*iCategoryList)[i].ExternalizeL(aStream);
To a large extent it is personal preference as to which of the above is used within an application. One point of detail that may influence your decision is the fact that the code performing the store can leave. When using the C++ << operator, it is not particularly obvious that << can leave.
InternalizeL()
The InternalizeL() method of our TAppCategoryEntry is:
void TAppCategoryEntry::InternalizeL(RReadStream& aStream)
{
TInt version=aStream.ReadUint8L();
if (version<=KAppCategoryEntryStreamVersion)
{
// iCategoryId.
iCategoryId=aStream.ReadInt32L();
// iCategoryName.
iCategoryName.SetLength(aStream.ReadUint8L());
aStream.ReadL((TText*)(iCategoryName.Ptr()), iCategoryName.Length());
}
else
User::Leave(KErrNotSupported);
}
When compared with the ExternalizeL() method you should observe that items are internalized in the same order they were externalized, that is version information, category ID, then category name. Your application must ensure that the amount of content it reads for a particular field matches the amount of data it stored. For example, since we stored a single byte for the version information we need to read a single byte so the stream remains aligned.
The >> operator
As with ExternalizeL(), Symbian OS contains a template for the >> operator. This template expects to see a method name of InternalizeL() taking a single RReadStream& parameter belonging to the object to which it is applied. If we choose to call our object loading function InternalizeL(RReadStream&) then we can choose whether to use the >> operator or call the method directly. We have named our function such that we can use the >> operator. The engine can load the set of categories with the following code:
TInt count=aStream.ReadInt16L();
for (i=0;i<count;i++)
{
TAppCategoryEntry catEntry;
aStream>>catEntry;
catList->AppendL(catEntry);
}
Again, whether you use >> or InternalizeL() is down to personal preference. As with the << operator you should be aware that the >> operator can leave.
| Pro tip:
It is possible to call your load and save methods anything you like, or indeed have methods that take a different set of parameters should your application require it. The potential problem with this approach is that you cannot use the |
Streams and Version Information
It is up to your application what information it chooses to save to a stream and what order that information is stored in.
One piece of information we recommend storing at various points is some class or structure version information. Should you ever need to update a particular object, a version number within the stream data enables you to remain backwards compatible. Since it tends to be individual objects that change, it is common to store a version number with an object as opposed to storing a single higher level version number. This is another example of encapsulation and is demonstrated in our example application.
| Pro tip:
There is a trade off with storing version numbers. If you have a large collection of a single type of object and you store a version number for every instance of that object you need one more byte for each instance you save to disk. Conversely if you have a single version number stored outside the collection you have a bit more work to do when loading the content. In particular, you have to pass the version information to the |
How to Approach Writing the InternalizeL() and ExternalizeL() Code
The ExternalizeL() code is quite straightforward. In most cases you simply need to run though your object network and save the property you deem is required such that the application can be re-started as though it had never exited.
On the other hand the InternalizeL() method tends to be much more complicated. In general, as well as loading the information it also has to create the object network. Because of this we strongly recommend that you write the InternalizeL() method first This allows you to define the most convenient order in which content is saved, such that you can reconstruct your object network.
As previously discussed, it is often not sufficient to simply leave when an error occurs. Apart from cleaning up any resources, your application generally needs to roll back to a stable state. The InternalizeL() method is no different. In our application we need to create the category list, folder entry list and index and only when all three are successfully constructed can we replace any existing versions of those structures with the new ones.
| Pro tip:
|
Even with our example application you can imagine a future version that allows a user to move around the folder hierarchy and want to display previously visited folder content with folder specific sort and category information.
When you come to consider some of the Symbian Signed test requirements you should observe that rollback of the InternalizeL() method gains additional significance. In particular, our application becomes quite robust and can survive a number of error conditions the application is expected to handle.
The Purpose of Internalizing
A modern user expectation is that an application resumes from the point at which it was left. However, at a detail level it may be more important to save some states than others. For example, if your application supports a sort function it would be quite reasonable to expect the application to retain that information and present its data in the last sort order chosen by the user. However, saving exactly which entry had the highlight when the application exited may be a step too far.
For our example application we have:
- a list of user defined categories
- a list of folder entries with some associated data
- some presentation information, such as currently chosen category and sort order.
We have chosen to internalize all those details we consider important but have chosen not to internalize everything, such as which entry within the list view contains the highlight or whether the list or detail view was the active view. We consider this acceptable for our application. Your application may have different requirements.
The CAppEngine::InternalizeL() Functionality
Our example application is slightly atypical since it is viewing the content of the file system. Other file management applications may be able to view and manipulate the same content, particularly if we are viewing publicly accessible folders. Consequently, we need to be somewhat defensive in our programming approach to internalizing our application state.
To recap, our key requirements are to internalize the set of categories, internalize our folder information and present the list of content as previously chosen by the user.
When we come to internalize our folder content, we need to be aware of several issues:
- additional folder entries may have been created
- some of the folder entries we originally listed may have been renamed or removed.
Our application handles these conditions by storing the category information along with a filename in our stream store. When we internalize the folder entry list, rather than simply taking the list of entries we externalized we have to create a list of what is currently on the disk, locate those entries in our internalized data and assign the category information.
TParse parse;
parse.Set(KWildCardName,&path,NULL);
CDir *dirList;
User::LeaveIfError(iFs.GetDir(parse.FullName(),
KEntryAttNormal|KEntryAttReadOnly|KEntryAttHidden|KEntryAttSystem|KEntryAttDir,
0,dirList));
CleanupStack::PushL(dirList);
// Add a set of entries to the new container.
TFolderEntry entry;
count=dirList->Count();
for (i=0;i<count;i++)
{
// Construct a new entry.
entry.Construct((*dirList)[i]);
// Add to the new list of entries.
folderEntryList->AppendL(entry);
}
// Now we have finished with the dirList and
// its top of cleanup stack we can delete it.
CleanupStack::PopAndDestroy(dirList);
// Load the list of entries we knew about before exit.
count=aStream.ReadInt32L();
RArray<TFolderEntry>* savedEntryList = new(ELeave)RArray<TFolderEntry>(8);
CleanupClosePushL(*savedEntryList);
for (i=0;i<count;i++)
{
aStream>>entry;
savedEntryList->AppendL(entry);
}
// Update our new entry list with
// saved category entry information.
TInt entryCount=folderEntryList->Count();
for (i=0;i<entryCount;i++)
{
TFolderEntry& entry=(*folderEntryList)[i];
const TDesC& name=entry.EntryName();
for (TInt j=0;j<count;j++)
{
TFolderEntry& entry2=(*savedEntryList)[j];
if (name==entry2.EntryName())
{ // Found the entry we saved the category info for.
entry.UpdateCategory(entry2.EntryCategory());
break;
}
}
}
CleanupStack::PopAndDestroy










