Symbian programmering - del 2

I andra delen av vår mobilskola får du lära dig att skriva ett program som gör det enkelt att göra minnesanteckningar efter avslutade telefonsamtal.

Publicerad Senast uppdaterad

I del 1 tittade vi på hur man kom igång med Symbian utveckling för Sony Ericssons telefoner, t ex M600. I detta nummer tar vi upp ett mera omfattande, mer realistiskt och eventuellt användbart exempel. All källkod finns på mobil.se, av utrymmesskäl kan vi inte få med allt tidningen.

Tanken är att göra en enkel applikation som efter ett avslutat telefonsamtal ger användaren möjlighet att skriva en notering om samtalet som sedan lagras i "anteckningsfältet" för den man just pratat med. En förutsättning är då förstås att den man pratat med finns lagrad som en kontakt i telefonen.

Komma igång

Vi börjar på samma sätt som i förra numret, vi skapar ett skal för en applikation m h a Carbides wizard. Starta Carbide.c++ och välj File | New | C++ Application for UIQ Project i menyn. Ge ditt projekt ett namn, t ex ''SamtalsNotering". Välj template "UIQ 3.x GUI Application". Gå sedan igenom resten av guiden, man behöver inte mata in något mer eller ändra något av de förvalda alternativen. Detta exempel är lämpligast att bygga direkt för telefonen och testar på den.

Allmänt om aktiva objekt

Symbian-applikationer använder sig normalt bara av en tråd. För att kunna göra flera saker "samtidigt" så använder man sig istället av något som kallas aktiva objekt. I praktiken så innebär det att man definierar och implementerar egna klasser som ärver från klassen CActive.

Ofta har då en applikation flera olika aktiva objekt som väntar på att olika händelser skall inträffa och när händelsen inträffar tar man hand om den i klassens RunL() metod.

I detta sammanhang pratar man också om asynkrona metoder. I olika APIer finns det ofta både synkrona och asynkrona varianter av en funktion som gör samma sak. Den synkrona varianten kommer att blockera applikationens tråd till dess att funktionen är klar. Tar funktionen lång tid att utföra så kommer applikationen inte vara mottaglig för input från användaren under tiden och det är inte alls bra. En asynkron metod sätter bara igång funktionen och kodraden efter exekveras omgående. Asynkrona metoder anropar man oftast från ett aktivt objekt. När sedan funktionen är klar anropas det aktiva objektets RunL() metod. Under tiden har applikationen kunnat göra andra saker, lyssna på användarinput och andra aktiva objekt kan ha körts.

CMyActive klassen

Vi börjar att utöka vår applikation med en ny klass CMyActive.

Vi lägger därför till filerna:

\inc\CMyActive.hklass definition

\src\CMyActive.cppImplementation av klassens metoder

Header-filen ser initialt ut så här:

KOD START

#ifndef CMYACTIVE_H_

#define CMYACTIVE_H_

#include

//Aktivt objekt som prenumerarar på samtalsinformation

class CMyActive: public CActive

{

public:

static CMyActive* NewL();

~CMyActive();

void Start();

protected:

//Implementerar metoder definierade i basklassen CActive

void RunL();

void DoCancel();

TInt RunError(TInt aError);

private:

CMyActive();

void ConstructL();

private:

enum TState

{

EStart,

EWaitingForCall,

ECallInProgress,

EFindingContact,

EUpdatingContact

};

TState iState; //Tillstånd

};

#endif /*CMYACTIVE_H_*/

KOD SLUT

Notera hur klassen ärver från CActive och att man måste definiera metoderna RunL och DoCancel. Ofta är ett aktivt objekt en tillståndsmaskin, det är därför vi har definierat medlemsvariablen iState som kan anta ett antal olika värden.

Hur en första implementation i CMyActive.cpp ser ut kan du se i exempel1.txt

NewL() metoden är Symbians sätt att skapa objekt, så kallad tvåfas-konstruktion. Detta är intimt förknippat med några andra grundbultar i Symbian-programmering nämligen Cleanup-stacken och TRAP/Leave. Dessa tekniker används för att hantera möjliga felsituationer speciellt då att det är slut på internminne (RAM) i telefonen och programmet inte kan allokera mer minne. Allt detta beskrivs mycket detaljerat i SDK dokumentation och annan Symbian dokumentation så vi går inte in i mer detalj här.

Hålla reda på telefonsamtals tillstånd och hämta information om samtal

Vi vill veta när ett telefonsamtal kopplas upp och när det sedan avslutas. För detta kommer vi använda klassen CTelephony så vi måste inkludera:

KOD START

#include //CTelephony med mera

KOD SLUT

I CMyActive klassen definierar vi också en medlemsvariabel som är en pekare till ett CTelephony objekt plus några andra medlemsvariabler som används för att hämta ut information om samtalet:

KOD START

CTelephony* iTelephony;

CTelephony::TCallStatusV1 iLineStatus;

CTelephony::TCallStatusV1Pckg iLineStatusPckg;

KOD SLUT

I CMyActive::ConstructL() så skapar vi vårt CTelephony objekt:

KOD START

void CMyActive::ConstructL()

{

//Skapa objekt för telefoni

iTelephony = CTelephony::NewL();

:

KOD SLUT

iLineStatus/iLineStatusPckg variablerna måste initieras.

KOD START

CMyActive::CMyActive():CActive(EPriorityStandard),

iLineStatusPckg(iLineStatus)

{

CActiveScheduler::Add(this); //Måste göras för varje aktivt objekt

}

KOD SLUT

Kom även ihåg att ta bort detta objekt i klassens destruktor annars läcker vi minne.

KOD START

CMyActive::~CMyActive()

{

Cancel();

//Ta bort alla objekt vi skapade i konstruktor

delete iTelephony;

}

KOD SLUT

Nu kommer vi till det aktiva objektets kärna, ett asynkront anrop (prenumeration) och RunL() metoden där vi hanterar att händelsen har inträffat.

KOD START

void CMyActive::Start()

{

//Lyssna/prenumerera på samtalshändelser, ändringar av s k "call state"

iTelephony->NotifyChange(iStatus, CTelephony::EVoiceLineStatusChange, iLineStatusPckg);

SetActive();

iState = EWaitingForCall; //Nytt tillstånd, vi väntar på att ett samtal kopplas upp

}

KOD SLUT

Här anropar vi NotifyChange och definierar att vi vill bli informerade när "voice line status" ändras. Telefonlinjens tillstånd kommer vi att kunna läsa ut ur variabeln iLineStatusPckg som vi skickar med funktionsanropet. Notera att asynkrona metoder alltid har ett argument av typen TRequestStatus. Alla aktiva objekt har en medlemsvariabel, iStatus av denna typ, och denna skickar vi med i anropet.

RunL metoden blir ganska omfattande så det finns inte utrymme att ha med all kod i tidningen utan vi hänvisar till hemsidan.

I RunL kommer vi att hamna varje gång samtalet ändrar tillstånd. Tillståndet förändras typiskt så här för ett inkommande samtal: Ringing - Answering - Connected - (Disconnecting) - Idle.

När telefonlinjens status ändras till Connected kommer vi att lagra undan starttid. Dessutom kommer vi använda metoden CTelephony::GetCallInfo för att hämta ut:

-Inkommande eller utgående samtal

-Numret på den man pratar med om det finns tillgängligt.

KOD START

if (iLineStatus.iStatus == CTelephony::EStatusConnected)

{

//Ett samtal har kopplats upp

TTime startTime;

startTime.HomeTime(); //Nu

iSamtalsInfo.iStart = startTime; //lagra undan start-tid

iState = ECallInProgress; //Nytt tillstånd

callSelectionV1.iSelect = CTelephony::EActiveCall;

//Hämta information om detta samtal

TInt err = iTelephony->GetCallInfo(callSelectionV1Pckg, callInfoPkg, remotePartyInfoPkg);

if (err == KErrNone)

{

CTelephony::TTelNumber number = remotePartyInfo.iRemoteNumber.iTelNumber;

CTelephony::TTelNumber dialledNumber = callInfo.iDialledParty.iTelNumber;

iSamtalsInfo.iDirection = remotePartyInfo.iDirection;

if (remotePartyInfo.iDirection == CTelephony::EMobileTerminated)

iSamtalsInfo.iTelNumber = number; //Incoming call

else

iSamtalsInfo.iTelNumber = dialledNumber; //Outgoing call

:

KOD SLUT

När status sedan ändras till EStatusIdle så räknar vi ut samtalets längd.

All information vi får fram lagrar vi en medlemsvariabel iSamtalsInfo av typen TSamtalsInfo som vi själva definierat i klassen CMyActive

KOD START

//Struktur där vi lagrar information om ett samtal

class TSamtalsInfo

{

public:

TTime iStart;//Starttid

TTimeIntervalSeconds iDuration;//Samtalets längd

CTelephony::TTelNumber iTelNumber;//Telefonnummer

CTelephony::TCallDirection iDirection;//Inkommnade/Utgående samtal

TContactItemId iCntId;//id på matchande kontakt

};

KOD SLUT

KOD START

TSamtalsInfo iSamtalsInfo;

KOD SLUT

Andra tillståndsförändringar ignorerar vi, då anropar vi bara NotifyChange() igen så vi kommer att bli anropade när nästa tillståndsförändring sker.

KOD START

iTelephony->NotifyChange(iStatus, CTelephony::EVoiceLineStatusChange, iLineStatusPckg);

SetActive();

KOD SLUT

När man använder en ny klass som CTelephony i sin applikation så kommer man få länkningsfel om man inte pekar ut motsvarande bibliotek (lib) i sitt projekt.

DUMP1

Man kan lägga till bibliotek i projektets properties.

Gör Add, och lägg till följande fil:

DUMP2

Notera att när man jobbar i emulatorn så lägger man till motsvarande *.lib fil som ligger under \epoc32\release\winscw\udeb.

I och med att vi använder CTelephony::GetCallInfo så måste vi ge applikationen en capability, ReadUserData. Även detta görs i Properties.

Sök i kontaktdatabasen

När ett samtal är avslutat så har vi samlat på oss information om starttid, samtalets längd och förhoppningsvis telefonnummer för den vi pratat med. Numret använder vi nu för att göra en sökning i kontaktdatabasen.

Vi börjar med att utöka CMyActive klassen med följande medlemsvariabler och de inkludera motsvarande header-filer.

KOD START

CContactDatabase* iContactsDb;

CContactItemFieldDef* iCntFieldDef;

CIdleFinder* iFinder; //Används för sökning i kontakter

KOD SLUT

Sedan skapar vi några av dessa i klassens konstruktor.

KOD START

void CMyActive::ConstructL()

{

//Skapa objekt för telefoni och kontakt-hantering

iTelephony = CTelephony::NewL();

iContactsDb = CContactDatabase::OpenL();

iCntFieldDef = new(ELeave) CContactItemFieldDef();

iCntFieldDef->AppendL(KUidContactFieldPhoneNumber); //search only tel numbers

iState = EStart;

}

KOD SLUT

Vi kommer att använda metoden FindAsyncL() i CContactDatabase.

KOD START

CIdleFinder *FindAsyncL(const TDesC &aText, const CContactItemFieldDef *aFieldDef, MIdleFindObserver *aObserver);

KOD SLUT

aText är den sträng(telefonnummer) vi letar efter, aFieldDef är de fält som man vill leta i, vi har definierad att bara leta i fält som lagrar telefonnummer. Det här asynkrona anropet har inget TRequestStatus argument, utan använder sig av en annan teknik. Här skall man skicka med en pekare till ett objekt som man vill skall bli anropat med resultatet av sökningen.

I detta fall vill vi att vår CMyActive klass skall vara denna observer. Därför låter vi CMyActive ärva även från MIdleFindObserver.

KOD START

class CMyActive: public CActive, public MIdleFindObserver

KOD SLUT

Att ärva från denna klass (eller så kallade mixin interface) innebär att vi måste implementera metoden IdleFindCallback() i CMyActive. Det är genom denna callback funktion vi kommer att få veta hur sökningen fortlöper.

Innan vi drar igång sökningen så tar vi och manipulerar det telefonnummer vi vill söka efter. Vi behåller bara de 8 sista siffrorna, detta för att undvika problem med hur nummer lagras i kontakter, t ex med +46, mellanslag, inledande nollor.

KOD START

//Använd bara 8 sista siffrorna vid sökning, bli av med +46, 0, etc. 7 annat vanligt //värde också,

CTelephony::TTelNumber strippedNumber = iSamtalsInfo.iTelNumber.Right(8);

// Dra igång asynkron sökning i kontakt-databasen,

// IdleFindCallBack kommer rapportera hur sökningen går!

iFinder = iContactsDb->FindAsyncL(strippedNumber, iCntFieldDef, this);

KOD SLUT

this = pekare till sig själv, dvs vi talar om för kontaktdatabasen att det är vårt CMyActive-objekt där det finns en IdleFindCallback metod som skall anropas för att rapportera hur sökningen går.

Hur vår implementation av IdleFindCallback ser initialt ut kan du se i exempel2.txt.

IdleFindCallback kommer anropas ett flertal gånger men vi väntar tills vi får veta att sökningen är klar. Om det finns ett eller flera träffar så lagrar vi undan id för den första träffen.

Ytterligare en observer

Nu har vi all information inklusive kontaktid. Nu vill vi låta användaren mata in en notering och lagra den tillsammans med annan information om samtalet i motsvarande kontakt. Innan vi visar en dialog vill vi att vår applikation får fokus. Detta är lättast att implementera i AppUi-klassen och därför vill vi att CMyActive skall anropa AppUi-klassen. Detta löser vi med att göra en egen observer-klass. Lägg till följande i början av CMyActive.h

KOD START

// Mixin-klass (motsvarande Javas interface)

// HandleContactLookupComplete metod måste implementeras i // ärvd klass

class MMyObserver

{

public:

virtual void HandleContactLookupComplete(const TSamtalsInfo& aSamtalsInfo)=0; //Pure virtual metod

protected:

virtual ~MMyObserver() {}

};

KOD SLUT

Sedan lägger vi till en medlemsvariabel i CMyActive.

KOD START

MMyObserver* iObserver;

KOD SLUT

Och ändrar vårt interface till CMyActive något.

KOD START

static CMyActive* NewL(MMyObserver* aObserver);

KOD SLUT

KOD START

CMyActive(MMyObserver* aObserver);

KOD SLUT

KOD START

CMyActive::CMyActive(MMyObserver* aObserver):CActive(EPriorityStandard),

iObserver(aObserver), iLineStatusPckg(iLineStatus)

{

KOD SLUT

Detta innebär också att konstruktionen av vårt CMyActive objekt måste ändras.

KOD START

iTeleMonitor = CMyActive::NewL(this);

KOD SLUT

I vår AppUi-klass så ärver vi nu även från MMyObserver och implementerar callback-funktionen HandleContactLookupComplete.

KOD START

class CSamtalsNoteringAppUi : public CQikAppUi, public MMyObserver

{

public:

// from CQikAppUi

void ConstructL();

~CSamtalsNoteringAppUi();

void HandleContactLookupComplete(const TSamtalsInfo& aSamtalsInfo);

KOD SLUT

Implementationen av callback-funktionen ser ut så här. Vi kommer att återkomma till iDialogHandler.

KODSTART

void CSamtalsNoteringAppUi::HandleContactLookupComplete(const TSamtalsInfo& aSamtalsInfo)

{

//Aktivera vår egen applikations vy så vår app får fokus:

TVwsViewId viewId(KUidSamtalsNoteringApp,KUidSamtalsNoteringView);

TRAPD(err, ActivateViewL(viewId));

iDialogHandler->Start(aSamtalsInfo);

//Börja lyssna efter nästa samtal.

iTeleMonitor->Start();

}

KOD SLUT

Till slut måste vi se till att callback-funktionen anropas från CMyActive klassen efter att vår sökning lyckats.

KOD START

if (idArray->Count() >= 1)

{

//1 eller flera träffar

iSamtalsInfo.iCntId = (*idArray)[0];

//Anropa vår observer = AppUi-objektet

iObserver->HandleContactLookupComplete(iSamtalsInfo);}

KOD SLUT

En enkel (asynkron) dialog

Vi definierar ytterligare en klass som är ett aktivt objekt. Denna klass följer samma mönster som CMyActive. Hur den ser ut kan du se i exempel3.txt

I Start metoden kommer vi att dra igång en dialogbox som inte blockerar vårt program. När användaren stänger dialogen med någon knapp så kommer vår RunL metod att anropas.

KOD START

void CAsyncDialogHandler::Start(const TSamtalsInfo& aSamtalsInfo)

{

//Lagra undan samtalsinfo i objektets medlemsvariabel.

iSamtalsInfo = aSamtalsInfo;

if (!IsActive())

//Om dialogen fortfarande visas för föregående samtal, så struntar vi att ta hand om //nästa samtal....kan göras bättre såklart

{

iStatus = KRequestPending; //Workaround pga ofullkomlighet i UIQ3 Simple dialog

iDialog = new (ELeave) CQikSimpleDialog(); //Skapa dialog objekt

iDialog->PrepareL(R_DLG_OK_CANCEL); //Läs in layout från resursfil

iDialog->RunL(iStatus); //Kör/visa dialogen, metoden returnerar omedelbart

SetActive();

}

}

KOD SLUT

Dialogens utseende definieras som en struktur, R_DLG_OK_CANCEL, i resursfilen SamtalsNotering.rss, se källkoden på vår hemsida, i filen exempel 4

Uppdatera information i kontaktdatabasen

Metoden CreateCallLogTextL skapar en textsträng som innehåller tidpunkt för samtalet, samtalets längd, inkommande eller utgående samtal och den text som användaren matat in i dialogboxen. UpdateContactNotesFieldL tar sedan texten och lägger in den under rätt kontakt. Se exempel5.txt

I och med att vi ändrar data i kontakter så måste vi ge vår applikation ytterligare en capability, WriteUserData.