vendredi 18 novembre 2011

Créer un client mail sur Symbian

Je vous proprose de créer une petite application Qt permettant d'envoyer des emails depuis son téléphone Symbian. Voici le rendu final :


N'étant pas infographiste, soyez indulgent pour la qualité visuelle. Avant de me lancer dans des explications de code, je tiens à préciser plusieurs choses :
  1. J'ai utilisé la dernière version du sdk Qt, à savoir la version 1.1.4, avec certains des composants Qt Quick pour Symbian.
  2. J'ai validé cette application sur un Nokia E7 upgradé avec Symbian Anna. La config de compilation Qt utilisée est donc "Qt 4.7.4 pour Symbian Anna".
  3. L'application ne permet pas d'envoyer un email avec le simulateur
  4. Il vous faut créer au moins un compte email sur votre téléphone Symbian
  5. J'ai fait le choix de gérer la partie purement graphique en QML et la partie logique métier en C++
Partie C++

J'ai créé une classe "helper" dénommée "EmailHelper" comme suit :

class EmailHelper : public QObject
{
    Q_OBJECT
public:
    explicit EmailHelper(QObject *parent = 0);
    ~EmailHelper();
    Q_INVOKABLE bool sendEmail(QString accountName, QString recipient, QString subject, QString body);
    Q_INVOKABLE QStringList getEmailAccountList();
signals:
    void stateMsg(const QString &statemsg);
    void errorMsg(const QString &errormsg);
private slots:
    void messageStateChanged(QMessageService::State s);
    
private:
    QMessageService iMessageService;
    QMessageManager iManager;
    QMessageService::State state;
    QMap<QString, QMessageAccountId> accountList;
};

Comme vous le constatez cette classe me permet de récupérer la liste des comptes email configurés sur le téléphone (méthode getEmailAccountList) et d'envoyer un email depuis un compte donné (méthode sendEmail). Notez la signature particulière de ces deux méthodes : j'ai utilisé la macro "Q_INVOKABLE" afin de les rendre accessible depuis le contexte d'execution déclaratif QML. Enfin pour gérer l'envoi d'email, je m'appuie sur le module "Messaging" de Qt Mobility via l'usage des deux classes QMessageService et QMessageManager.

Coté implémentation, rien de bien compliqué :

  • Un constructeur où je m'abonne au signal "stateChanged" émis par le service de messagerie:
EmailHelper::EmailHelper(QObject *parent) :
    QObject(parent)
{
    state = QMessageService::InactiveState;
    connect(&iMessageService, SIGNAL(stateChanged(QMessageService::State)), this, SLOT(messageStateChanged(QMessageService::State)));
}
  • La méthode de récupération des comptes emails :
QStringList EmailHelper::getEmailAccountList()
{
    QStringList accountNameList;

#ifdef SENDEMAIL_ENABLED
    // Récupération de la liste des comptes email et ajout dans la liste
    foreach (const QMessageAccountId &id, iManager.queryAccounts())
    {
        QMessageAccount account(id);
        if (account.messageTypes() & QMessage::Email)
        {
            QString name(account.name());
            accountList.insert(name, account.id());
            accountNameList.append(name);
        }
    }
#endif
    return accountNameList;
}

Note : le service de messagerie étant commun aux SMS, MMS et Email, je mets un filtre en place sur les emails. Globalement un compte email est composé d'un libellé et d'un identifiant ; je stocke ces informations dans une propriété de type QMap afin de ré-utiliser la correspondance Libellé/Identifiant utlérieurement pour l'envoi d'un email. Et je retourne uniquement la liste de libellés de comptes à destination de l'affichage géré en QML

  • La méthode d'envoi d'un email : 

bool EmailHelper::sendEmail(QString accountName, QString recipient, QString subject, QString body)
{
#ifdef SENDEMAIL_ENABLED
    if (!QMessageAccount::defaultAccount(QMessage::Email).isValid())
    {
        emit errorMsg("Aucun compte configuré pour envoyer un email.");
        return false;
    }
    if (state == QMessageService::InactiveState || state == QMessageService::FinishedState)
    {
        QMessage message;
        QMessageAccountId accountId = accountList[accountName];
        message.setType(QMessage::Email);
        message.setParentAccountId(accountId);
 
        // Email de destination
        QMessageAddress::Type addrType(QMessageAddress::Email);
        QMessageAddressList toList;
        toList.append(QMessageAddress(addrType, recipient));
        message.setTo(toList);
 
        // Objet du mail
        if (subject.isEmpty()) {
            emit errorMsg("Aucun objet spécifié");
            return false;
        }
        else
            message.setSubject(subject);
 
        // Corps du mail
        if (body.isEmpty()) {
            emit errorMsg("Aucun contenu de message spécifié");
            return false;
        }
        else
            message.setBody(body);
 
        // Envoi de l'email
        bool result = iMessageService.send(message);
        return result;
    }
    else
    {
        return false;
    }
}

Cette méthode initialise un objet QMessage à partir du compte email séléctionné par l'utilisateur avec :
1. Une adresse de destination
2. Un objet
3. Un corps de mail

Note : pour aller plus loin dans la prise en main de l'api de messaging, je vous invite à ajouter une pièce jointe.

Afin de rendre ma classe visible et instantiable dans le contexte déclaratif QML, j'ai ajouté la ligne suivante dans la fonction "main" du programme :

qmlRegisterType<EmailHelper>("EmailHelper",1,0,"EmailHelper");

Ainsi, comme nous le verrons plutard, je pourrai instantier mon objet depuis QML via une simple déclaration comme cela :

EmailHelper {
}
  • Pour finir j'ai implémenté le slot privé "messageStateChange" dont le rôle est de relayer le signal public "stateMsg" à la partie QML afin d'en informer de manière asynchrone du statut de l'envoi d'email :
void EmailHelper::messageStateChanged(QMessageService::State s){
       state = s;
    if (s == QMessageService::InactiveState)
    {
        emit stateMsg("InactiveState");
    }
    else if (s == QMessageService::ActiveState)
    {
        emit stateMsg("ActiveState");
    }
    else if (s == QMessageService::CanceledState)
    {
        emit stateMsg("CanceledState");
    }
    else if (s == QMessageService::FinishedState)
    {
        emit stateMsg("FinishedState");
    }
    else
    {
        emit stateMsg(QString("QMessageService::%1").arg(s));
    }
}

 
Et voilà pour la partie C++

Partie Environnement :

Afin de pouvoir compiler et envoyer un email de manière effective, il est INDISPENSABLE de modifier son fichier projet (*.pro) comme suit :
  • Ajouter les deux lignes suivantes pour l'utilisation du module "Messaging" de Qt Mobility :
CONFIG += mobility
MOBILITY += messaging
  • Ajouter les permissions suivantes (notamment pour accéder aux comptes email de l'utilisateur) :
symbian:TARGET.CAPABILITY += NetworkServices \
                             LocalServices \
                             ReadUserData \
                             WriteUserData \
                             UserEnvironment \
                             ReadDeviceData \
                             WriteDeviceData
  • Ajouter la dépendance aux composants Qt Quick
CONFIG += qt-components

Dernier point : il faut que l'application soit signée avec un certificat "Express Signed" ; le mode "auto-signé" ne fonctionne pas car nous avons besoin de la permission "ReadDeviceData". Donc soit vous utiliserez un certificat fourni par PublishToOVI via votre compte Nokia Publisher, soit vous utiliserez un certificat SymbianSign.

Partie UI en QML :

Ayant utilisé le template de projet Qt Quick pour Symbian avec utilisation des composants Qt Quick, je me retrouve donc avec deux fichiers :

1. main.qml
2. MainPage.qml

J'ai mis tout le code dans le fichier MainPage.qml ; pour un vrai projet il est recommendé de modulariser ses composants via une arborescence de fichiers bien organisés.

Commençons par les inclusions de module :
  • import com.nokia.symbian 1.1 : pour utiliser les composants Qt Quick
  • import Qt.labs.components 1.1 : pour utiliser le regroupement exclusif des boutons radios
  • import EmailHelper 1.0 : notre classe "helper" pour envoyer des emails
Ensuite j'ai crée un petit fond à base de gradients de couleur :

Rectangle {
    id: rootWindow
    anchors.fill: parent
    width: 360
    height: 640

    gradient: Gradient {
        GradientStop {
             position: 0
             color: "#ccc094"
        }
        GradientStop {
             position: 0.8
             color: "#777057"
        }
        GradientStop {
             position: 1
             color: "#000000"
        }
    }

Je définis ensuite une petite boîte de dialogue servant à afficher les messages d'erreur et de succés :

CommonDialog {
    id: dialog
    titleText: "Information"
    buttonTexts: ["OK"]
    content: Rectangle {
                 width: 300
                 height: 200
                 anchors.horizontalCenter: parent.horizontalCenter
                 color: "transparent"
                 Text {
                    id: dialogMessage
                    anchors.centerIn: parent
                    textFormat: Text.StyledText
                    width:290
                    text:""
                    wrapMode: Text.Wrap
                    color: "white"
                 }
             }
    onButtonClicked: {
        if(mainPage.quitRequested == true) Qt.quit()
    }
}

Puis j'instancie mon élément "EmailHelper" :

EmailHelper {
    id: helper
    property int accountNumber;
    property variant accountNameList: []
    onStateMsg: {
        console.log(statemsg)
        if(statemsg == "FinishedState")
        {
            indicator.visible = false
            indicator.running = false
            dialogMessage.text = qsTr("Email envoyé avec succès")
            dialog.open()
        }
    }
    onErrorMsg: {
        console.log(errormsg)
        indicator.visible = false
        indicator.running = false
        dialogMessage.text = errormsg
        dialog.open()
    }
}

Comme vous pouvez le constater j'y défini les slots pour capturer les signaux "errorMsg" et "stateMsg" ; j'y rattache également la liste des noms de comptes emails via la variable "accountNameList" qui sera initialisée au chargement de la page QML comme le montre le code suivant :

Component.onCompleted: {
    console.log("Component.onCompleted")
    helper.accountNameList = helper.getEmailAccountList()
    helper.accountNumber = helper.accountNameList.length
    if( helper.accountNumber == 0)
    {
        mainPage.quitRequested = true
        dialogMessage.text = qsTr("Désolé vous n'avez aucun compte mail configuré sur ce téléphone")
        dialog.open()
    }
    else console.log("Vous avez "+ helper.accountNumber +" comptes");

    for(var i=0;i<helper.accountNumber;i++){
        console.log("Compte "+i+" : "+helper.accountNameList[i])
    }
 }

Par conséquent, la liste des comptes mails est récupérée au chargement de la page ; si cette liste est vide ou indisponible, j'affiche un popup invitant l'utilisateur à créer un compte email sur son téléphone.

Enfin j'affiche le formulaire de saisie permettant de contruire l'email à envoyer ; ce formulaire possède un header avec un bouton d'envoi :

// Fixed Header
Item {
    id: header
    width: rootWindow.width
    height: 60
    z:3
    Rectangle {
        color: "black"
        anchors.fill: parent
        BusyIndicator {
            id: indicator
            anchors.leftMargin: 5
            running: false
            visible: false
        }
        Text {
            id: title
            text: qsTr("Custom Mail Client")
            color: "white"
            anchors.centerIn: parent
            font.bold: true
            font.pixelSize:20
        }
        Button {
            id: send
            iconSource: "send.png"
            x:rootWindow.width-send.width
            anchors.rightMargin: 5
            width:header.height-5
            height:header.height-5
            onClicked: {
                if(mailAccount.account.length>0     &&
                   mailRecipient.recipient.length>0 &&
                   mailSubject.subject.length>0     &&
                   mailBody.body.length>0)
                {
                    var requestResult = helper.sendEmail(mailAccount.account, mailRecipient.recipient, mailSubject.subject, mailBody.body);
                    if(requestResult)
                    {
                        indicator.visible = true
                        indicator.running = true
                    }
                    else
                    {
                        dialogMessage.text = qsTr("Echec de la demande d'envoi d'email !");
                        dialog.open()
                    }
                }
                else if(mailRecipient.recipient.length == 0)
                {
                    dialogMessage.text = qsTr("Veuillez saisir une addresse de destination")
                    dialog.open()
                }
                else if(mailSubject.subject.length == 0)
                {
                    dialogMessage.text = qsTr("Veuillez renseigner un objet")
                    dialog.open()
                }
                else if(mailBody.body.length == 0)
                {
                    dialogMessage.text = qsTr("Veuillez renseigner le corps du mail")
                    dialog.open()
                }
            }
        }
    }
}

La partie formulaire sera positionnée dans une zone scrollable (Flickable) :

Flickable {
   id:flickable
   width: parent.width
   height:parent.height-header.height
   anchors.top: header.bottom
   contentWidth: flickable.width;
   contentHeight: mailAccount.height+flickable.height*3/2
   flickableDirection: Flickable.VerticalFlick

 
La partie bouton radio du formulaire pour sélectionner le compte email :

// Sélection du compte email
Item {
    property string account: helper.accountNameList[0]
    id: mailAccount
    width: rootWindow.width-20
    height: helper.accountNumber*50
    anchors.top: parent.top
    anchors.topMargin: 10
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: mailAccountLabel
        text: qsTr("Compte Mail :")
        font.bold: true
        font.pixelSize:20
    }
    CheckableGroup { id: group }
        Column {
            id: column
            anchors.top: mailAccountLabel.bottom
            anchors.topMargin: 5
            spacing: platformStyle.paddingMedium
            Repeater {
                model: helper.accountNumber
                RadioButton {
                     text: helper.accountNameList[index]
                     platformExclusiveGroup: group
                         onClicked: {
                             mailAccount.account = text
                         }
                     }
                 }
            }
        }

Ensuite il s'agit d'ajouter les zones de saisies et le tour est joué. Une fois les données saisies, l'utilisateur n'a plus qu'à valider le bouton situé dans le header du formulaire et l'email est envoyé.

// Destinataire du message
Item {
    property alias recipient: recipientRect.text
    id: mailRecipient
    width: rootWindow.width-20
    height: 50
    anchors.top: mailAccount.bottom
    anchors.topMargin: 25
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: mailLabel
        text: qsTr("Email de destination :")
        font.bold: true
        font.pixelSize:20
    }
    TextField {
        id: recipientRect
        anchors.top: mailLabel.bottom
        anchors.topMargin: 5
        width: parent.width
    }
}
 
// Objet du mail
Item {
    property alias subject: subjectRect.text
    id: mailSubject
    width: rootWindow.width-20
    height: 50
    anchors.top: mailRecipient.bottom
    anchors.topMargin: 25
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: subjectLabel
        text: qsTr("Objet :")
        font.bold: true
        font.pixelSize: 20
    }
    TextField {
        id: subjectRect
        anchors.top: subjectLabel.bottom
        anchors.topMargin: 5
        width: parent.width
    }
}
 
// Corps du mail
Item {
    property alias body: bodyInput.text
    id: mailBody
    width: rootWindow.width-20
    height: 150
    anchors.top: mailSubject.bottom
    anchors.topMargin: 25
    anchors.left: parent.left
    anchors.leftMargin: 10
    anchors.rightMargin: 10
    Text {
        id: bodyLabel
        text: qsTr("Corps du message :")
        font.bold: true
        font.pixelSize: 20
    }
    TextArea {
        id: bodyInput
        anchors.top: bodyLabel.bottom
        anchors.topMargin: 5
        width: parent.width
        height : 120
   }
}
 
Le code source complet du projet est téléchargeable ici 
Bonne lecture. 

Aucun commentaire:

Enregistrer un commentaire