在Ubuntu平台上开发快递邮件查询Scope

在这篇文章中,我们将介绍如何开发一个快递邮件查询的Scope。我们知道Scope很方便地把我们Web上的服务快速地集成到我们的系统中,并使之成为我们用户体验的一部分。我在先前的文章中重点介绍了如何使用点评Scope的开发,也介绍了中国天气的Scope的开。在这篇文章中,我们将从另外一个角度介绍一个新的Scope的开发。这个Scope和先前的使用的架构是不同的。它的department是从一个本地的文件中读出来的。希望对大家的开发有帮助。我们的最终Scope的显示如下:


     


1)创建一个最基本的Scope

在这个章节里,我们来创建一个最基本的Scope。大家一起来跟着我的步骤一步一步地来:



  






在这个例子里,我们选择使用的是“Empty Scope”。这样我们不必要去删除很多的文件。我们在Desktop上运行一下我们刚刚创建的Scope。没有什么是特殊的。我们使用能够热键Ctrl + R或SDK左下角的绿色的运行按钮,运行Scope:



如果你运行你的Scope,并看见如上图所示的画面,请按照如下的画面来重新设置你的configuration。




这是一个最基本的Scope,没有任何特殊的东西,因为这是个非常简单的Empty Scope。




2)加入Qt支持


由于“Empty Scope”模版对Qt没有进行支持。在这个章节里,我们教大家怎么把Qt加入到项目中。我们希望在项目中使用Qt来解析我们得到的数据。我们在项目中加入对Qt的支持。我们首先打开在“src”中的CMakeLists.txt文件,并加入如下的句子:

find_package(Qt5Core REQUIRED)     
find_package(Qt5Xml REQUIRED)      

include_directories(${Qt5Core_INCLUDE_DIRS})   
include_directories(${Qt5Network_INCLUDE_DIRS})

....

# Build a shared library containing our scope code.
# This will be the actual plugin that is loaded.
add_library(
  scope SHARED
  $<TARGET_OBJECTS:scope-static>
)

qt5_use_modules(scope Core Network) 

# Link against the object library and our external library dependencies
target_link_libraries(
  scope
  ${SCOPE_LDFLAGS}
  ${Boost_LIBRARIES}
)

)

我们可以看到,我们加入了对Qt Core库的调用。同时,我们也打开"tests/unit/CMakeLists.txt"文件,并加入“ qt5_use_modules(scope-unit-tests Core Network)"


# Our test executable.
# It includes the object code from the scope
add_executable(
  scope-unit-tests
  scope/test-scope.cpp
  $<TARGET_OBJECTS:scope-static>
)

# Link against the scope, and all of our test lib dependencies
target_link_libraries(
  scope-unit-tests
  ${GTEST_BOTH_LIBRARIES}
  ${GMOCK_LIBRARIES}
  ${SCOPE_LDFLAGS}
  ${TEST_LDFLAGS}
  ${Boost_LIBRARIES}
)

qt5_use_modules(scope-unit-tests Core Network)

# Register the test with CTest
add_test(
  scope-unit-tests
  scope-unit-tests
)

我们再重新编译我们的应用,如果我们没有错误的话,我们的Scope可以直接在desktop下直接运行。这样我们就完成了我们项目对Qt的支持。

3)快件查询API

我们可以在网址: http://www.kuaidi100.com/提供的服务来查询我们的快件。我们将使用如下的API来查询我们的快件的信息:

http://www.kuaidi100.com/query?type=shunfeng&postid=592833849048

这里 type可以是任何其它的公司。 postid是快件的单号。我们在这里也分别列出其它的快件公司的查询API接口:

[
    {
        "title": "顺丰",
	"pinyin": "shunfeng",
        "url": "http://www.kuaidi100.com/query?type=shunfeng&postid=%1"
    },
    {
        "title": "全峰",
	"pinyin": "quanfeng",
        "url": "http://www.kuaidi100.com/query?type=quanfengkuaidi&postid=%1"
    },
    {
        "title": "申通",
	"pinyin": "shentong",
        "url": "http://www.kuaidi100.com/query?type=shentong&postid=%1"
    },
    {
        "title": "EMS",
	"pinyin": "ems",
        "url": "http://www.kuaidi100.com/query?type=ems&postid=%1"
    },
    {
        "title": "圆通",
	"pinyin": "yuantong",
        "url": "http://www.kuaidi100.com/query?type=yuantong&postid=%1"
    },
    {
        "title": "中通",
	"pinyin": "zhongtong",
        "url": "http://www.kuaidi100.com/query?type=zhongtong&postid=%1"
    },
    {
        "title": "韵达",
	"pinyin": "yunda",
        "url": "http://www.kuaidi100.com/query?type=yunda&postid=%1"
    },
    {
        "title": "天天",
	"pinyin": "tiantian",
        "url": "http://www.kuaidi100.com/query?type=tiantian&postid=%1"
    },
    {
        "title": "汇通",
	"pinyin": "huitong",
        "url": "http://www.kuaidi100.com/query?type=huitongkuaidi&postid=%1"
    },
    {
        "title": "德邦",
	"pinyin": "debang",
        "url": "http://www.kuaidi100.com/query?type=debangwuliu&postid=%1"
    },
    {
        "title": "宅急送",
 	"pinyin": "zhaijisong",
       	"url": "http://www.kuaidi100.com/query?type=zhaijisong&postid=%1"
    }
]

这里我们使用了一个json结构的文件“departments.json”来存储这些信息,并把这个文件存于项目的“ data”目录下。这里的“ pinyin”项将被用作Scope中的department id。就像我们在这篇文章一开始的位置显示的那样,我们想把该Scope设计为一个department Scope。这样我们可以对所有的快递公司进行查询。我们可以对我们的一个实验性的 API进行展示如下:

{"nu":"592833849048","companytype":"shunfeng","com":"shunfeng","updatetime":"2014-12-23 18:13:16","signname":"","condition":"F00","status":"200","codenumber":"592833849048","signedtime":"","data":[{"time":"2014-11-29 13:19:43","location":"","context":"已签收,感谢使用顺丰,期待再次为您服务","ftime":"2014-11-29 13:19:43"},{"time":"2014-11-29 13:19:43","location":"","context":"在官网\"运单资料&签收图\", 可查看签收人信息","ftime":"2014-11-29 13:19:43"},{"time":"2014-11-29 11:14:23","location":"","context":"正在派送途中,请您准备签收(派件人:孙连杰,电话:13810320784)","ftime":"2014-11-29 11:14:23"},{"time":"2014-11-29 10:00:30","location":"","context":"快件到达 北京北苑集散中心","ftime":"2014-11-29 10:00:30"},{"time":"2014-11-29 08:45:55","location":"","context":"快件在 北京顺义集散中心, 正转运至 北京北苑集散中心","ftime":"2014-11-29 08:45:55"},{"time":"2014-11-29 06:29:29","location":"","context":"快件在 北京集散中心, 正转运至 北京顺义集散中心","ftime":"2014-11-29 06:29:29"},{"time":"2014-11-28 20:46:26","location":"","context":"快件在 厦门总集散中心, 正转运至 北京集散中心","ftime":"2014-11-28 20:46:26"},{"time":"2014-11-28 19:32:18","location":"","context":"快件在 厦门集美集散中心, 正转运至 厦门总集散中心","ftime":"2014-11-28 19:32:18"},{"time":"2014-11-28 17:26:37","location":"厦门莲岳服务点","context":"[厦门莲岳服务点]快件在 厦门莲岳服务点, 正转运至 厦门集美集散中心","ftime":"2014-11-28 17:26:37"}],"state":"3","departure":"厦门市","addressee":"","destination":"北京市","message":"ok","ischeck":"1","pickuptime":""}

在下面的章节中,我们将介绍如何使用这个API来对内容在Scope中进行显示。

为了在下面的练习中能够更加好地完成,我们可以在如下的地址下载


下载“images”,“renderer”目录及“departments.json”文件,并放到项目中的“ data”目录中。为了能够在Desktop上进行运行,我们也创建了如下的script文件“setup.sh”,并把该文件置于项目的根目录中:

#!/bin/bash

if [ $# -eq 0 ]; then
    DEST="../build-mailcheck-Desktop-Default/src/"

    mkdir -p $DEST
    cp -r data/departments.json $DEST/
    cp -r data/renderer $DEST/
    cp -r data/images $DEST/
    
    echo "Setup complete."
    exit 0;
fi

我们在我们项目的目录下打入如下的命令:



这样做的目的是把我们所需要在Desktop下运行所需要的文件拷到相应的目录中,以便我们在下面的程序中用到。通过这样的方法,我们把我们所需要的文件拷贝到如下的目录中以便我们在如下的练习中使用到。




重新运行编译我们的Scope以确保我们没有任何的错误。整个项目的源码在如下的地址:

bzr branch  lp:~liu-xiao-guo/debiantrial/mailcheck1


4)为Scope加入department

在这个章节中,我们准备为我们的Scope加入所需要的department。



我们知道我们所需要的department信息存在于一个叫做“deparments.json”的文件中。我们需要得到Scope的路径才可以得到这个文件。我们对scope.cpp做如下的修改:

sc::SearchQueryBase::UPtr Scope::search(const sc::CannedQuery &query,
                                        const sc::SearchMetadata &metadata) {
    const QString scopePath = QString::fromStdString(scope_directory());
    // Boilerplate construction of Query
    return sc::SearchQueryBase::UPtr(new Query(query, metadata, scopePath, config_));
}

通过得到的scopePath,我们把它传入到query.cpp中以便使用。记得在scope.cpp中加入

#include <QString>

以使得项目可以得到编译。

我们也对query.cpp要做相应的修改:

   Query(const unity::scopes::CannedQuery &query,
          const unity::scopes::SearchMetadata &metadata,  QString scopePath,
          api::Config::Ptr config);


我们在query.cpp中也存储了一些相应的全局变量来保存我们的状态:

QString g_scopePath;
QString g_rootDepartmentId;
QMap<QString, std::string> g_renders;
QString g_userAgent;
QString g_imageDefault;
QString g_imageError;
QMap<QString, QString> g_depts;
QString g_curScopeId;
static QMap<QString, QString> g_deptLayouts;

我们的Query类的构造函数如下:

#define LOAD_RENDERER(which) g_renders.insert(which, getRenderer(g_scopePath, which))

Query::Query( const sc::CannedQuery &query, const sc::SearchMetadata &metadata,
             QString scopePath, Config::Ptr config ) :
    sc::SearchQueryBase( query, metadata ), client_( config ) {
    g_scopePath = scopePath;
    g_userAgent = QString("%1 (Ubuntu)").arg(SCOPE_PACKAGE);
    g_imageDefault = QString("file://%1/images/%2").arg(scopePath).arg(IMG_DEFAULT);
    g_imageError = QString("file://%1/images/%2").arg(scopePath).arg(IMG_ERROR);

    // Load all of the predefined rederers. You can comment out the renderers
    // you don't use.
    LOAD_RENDERER( "journal" );
    LOAD_RENDERER( "wide-art" );
    LOAD_RENDERER( "hgrid" );
    LOAD_RENDERER( "carousel" );
    LOAD_RENDERER( "large" );
}

std::string Query::getRenderer( QString scopePath, QString name ) {
    QString renderer = readFile( QString("%1/renderer/%2.json" )
                                .arg(scopePath).arg(name));
    return renderer.toStdString();
}

QString Query::readFile(QString path) {
    QFile file(path);
    file.open(QIODevice::ReadOnly | QIODevice::Text);
    QString data = file.readAll();
    // qDebug() << "JSON file: " << data;
    file.close();
    return data;
}

我们通过如下的方法来解析“departments.json",并把“pinyin”项作为department id以便以后进行查询。虽然deparment id可以为任何一个字符串,只要保证它们之间是互相不同的。我们把得到的url数据存于一个叫做g_depts的全局变量中。

DepartmentList Query::getDepartments(QJsonArray data) {
    qDebug() << "entering getDepartments";

    DepartmentList depts;

    // Clear the previous departments since the URL may change according to settings
    g_depts.clear();
    qDebug() << "m_depts is being cleared....!";

    int index = 0;
    FOREACH_JSON( json, data ) {
        auto feed = (*json).toObject();
        QString title = feed["title"].toString();
//        qDebug() << "title: " << title;

        QString url = feed["url"].toString();
//        qDebug() << "url: " << url;

        QString pinyin = feed["pinyin"].toString();
//        qDebug() << "pinyin: " << pinyin;

        // This is the default layout otherwise it is defined in the json file
        QString layout = SURFACING_LAYOUT;

        if ( feed.contains( "layout" ) ) {
            layout = feed[ "layout" ].toString();
        }

        g_depts.insert( pinyin, url );
        g_deptLayouts.insert( pinyin, layout );

        CannedQuery query( SCOPENAME.toStdString() );
        query.set_department_id( url.toStdString() );
        query.set_query_string( url.toStdString() );

        Department::SPtr dept( Department::create(
                               pinyin.toStdString(), query, title.toStdString() ) );

        depts.push_back(dept);

        index++;
    }

    // Dump the departments. The map has been sorted out
    QMapIterator<QString, QString> i(g_depts);
    while (i.hasNext()) {
        i.next();
        qDebug() << "scope id: " << i.key() << ": " << i.value();
    }

    qDebug() << "Going to dump tthe department layouts";

    QMapIterator<QString, QString> j( g_deptLayouts );
    while (j.hasNext()) {
        j.next();
        qDebug() << "scope id: " << j.key() << ": " << j.value();
    }

    return depts;
}

为了能够使得我们的Scope能在手机或emulator上运行,我们还得对“data”目录下的“CMakeLists.txt”做如下的修改:

# Install the scope ini file
install(
  FILES "com.ubuntu.developer.liu-xiao-guo.mailcheck_mailcheck.ini"
  DESTINATION ${SCOPE_INSTALL_DIR}
)

# Install the scope images
install(
  FILES
    "icon.png"
    "logo.png"
    "screenshot.png"
    "departments.json"
  DESTINATION
    "${SCOPE_INSTALL_DIR}"
)

INSTALL(
    DIRECTORY "images"
    DESTINATION ${SCOPE_INSTALL_DIR}
)

INSTALL(
    DIRECTORY "renderer"
    DESTINATION ${SCOPE_INSTALL_DIR}
)

这样把“departments.json”,“images”及“render”都加入到项目中。

最终我们的结果如下:

   

我们可以看到我们的deparment了。所有的源码在如下的地址:

bzr branch  lp:~liu-xiao-guo/debiantrial/mailcheck2


5)完成surfacing及查询

上面我们已经产生了department列表,但是,我们还是不能使用它或查询到什么结果。下面我们添加如下的Search方法来得到查询的结果:

void Query::search(sc::SearchReplyProxy const& reply) {
    CategoryRenderer renderer(g_renders.value("journal", ""));
    auto search = reply->register_category(
                "search", RESULTS.toStdString(), "", renderer);

    CannedQuery cannedQuery = SearchQueryBase::query();

    QString deptId = QString::fromStdString(cannedQuery.department_id());
    qDebug() << "deptId: " << deptId;

    qDebug() << "m_rootDepartmentId: " << g_rootDepartmentId;
    QString url;

    qDebug() << "m_curScopeId: " << g_curScopeId;

    if ( !deptId.isEmpty() ) {
        g_curScopeId = deptId;
    }

    if ( deptId.isEmpty() && !g_rootDepartmentId.isEmpty()
         && g_curScopeId == g_rootDepartmentId ) {

        QMapIterator<QString, QString> i(g_depts);
        qDebug() << "m_depts count: "  << g_depts.count();

        qDebug() << "Going to set the surfacing content";

        const CannedQuery &query(sc::SearchQueryBase::query());
        // Trim the query string of whitespace
        string query_string = alg::trim_copy(query.query_string());
        QString queryString = QString::fromStdString(query_string);

        if ( queryString.isEmpty()) {
            url = QString(g_depts[g_rootDepartmentId]).arg(592833849048);
        } else {
            url = QString(g_depts[g_rootDepartmentId]).arg(queryString);
        }
    } else {
        QString queryString = QString::fromStdString(cannedQuery.query_string());
        qDebug() << "queryString: " << queryString;

        // Dump the departments. The map has been sorted out
        QMapIterator<QString, QString> i(g_depts);
        qDebug() << "m_depts count: "  << g_depts.count();

        while (i.hasNext()) {
            i.next();
            qDebug() << "scope id: " << i.key() << ": " << i.value();
        }

        url = g_depts[g_curScopeId].arg(queryString);
    }

    qDebug() << "url: "  << url;
    qDebug() << "m_curScopeId: " << g_curScopeId;

    try {
        QByteArray data = get(reply, QUrl(url));
        getMailInfo(data, reply);
    } catch (domain_error &e ) {
        cerr << e.what() << endl;
        reply->error(current_exception());
    }
}

void Query::getMailInfo(QByteArray &data, SearchReplyProxy const& reply) {
    QJsonParseError e;
    QJsonDocument document = QJsonDocument::fromJson(data, &e);
    if (e.error != QJsonParseError::NoError) {
        throw QString("Failed to parse response: %1").arg(e.errorString());
    }

    // This creates a big picture on the top
    CategoryRenderer rssCAR(CAR_GRID);
    auto catCARR = reply->register_category("A", "", "", rssCAR);
    CategorisedResult res_car(catCARR);
    res_car.set_uri("frontPage");
    QString defaultImage1 ="file://"+ g_scopePath + "/images/" + g_curScopeId + ".jpg";
    qDebug() << "defaultImage1: "  << defaultImage1;
    res_car["largepic"] = defaultImage1.toStdString();
    res_car["art2"] =  res_car["largepic"];
    reply->push(res_car);

    QJsonObject obj = document.object();

    qDebug() << "***********************\r\n";

    if ( obj.contains("data") ) {
        qDebug() << "it has data!";

        QJsonValue data1 = obj.value("data");

        QJsonArray results = data1.toArray();

        qDebug() << "g_curScopeId: " << g_curScopeId;
        QString layout = g_deptLayouts.value( g_curScopeId );
        std::string renderTemplate;

        if (g_renders.contains( layout )) {
            qDebug() << "it has layout: " << layout;
            renderTemplate = g_renders.value( layout, "" );
            // qDebug() << "renderTemplate: " << QString::fromStdString(renderTemplate);
        }
        else {
            qDebug() << "it does not have layout!";
            renderTemplate = g_renders.value( "journal" );
            // qDebug() << "renderTemplate: " << QString::fromStdString(renderTemplate);
        }

        CategoryRenderer grid(renderTemplate);
        std::string categoryId = "root";
        std::string categoryTitle = " "; // #1330899 workaround
        std::string icon = "";
        auto tracking = reply->register_category(categoryId, categoryTitle, icon, grid);

        FOREACH_JSON(result, results) {
            QJsonObject o = (*result).toObject();

            QString time = o.value("time").toString();
//            qDebug() << "time: " << time;

            QString context = o.value("context").toString();
//            qDebug() << "context: " << context;

            QString link = "http://www.kuaidi100.com/";
            QString defaultImage ="file://"+ g_scopePath + "/images/" + g_curScopeId + ".jpg";

            CategorisedResult result(tracking);

            SET_RESULT("uri", link);
            SET_RESULT("image", defaultImage);
            //            SET_RESULT("video", video);
            SET_RESULT("title", time);
            //            SET_RESULT("subtitle", context);
            SET_RESULT("summary", context);
            //            SET_RESULT("full_summary", fullSummary);
            //            result["actions"] = actions.end();

            if (!reply->push(result)) break;
        }
    }
}

QByteArray Query::get(sc::SearchReplyProxy const& reply, QUrl url) const {
    QNetworkRequest request(url);
    QByteArray data = makeRequest(reply, request);
    return data;
}

“Search”在“run”方法中被调用:

void Query::run(sc::SearchReplyProxy const& reply) {
    try {
        // Start by getting information about the query
        const CannedQuery &query(sc::SearchQueryBase::query());

        // Trim the query string of whitespace
        string query_string = alg::trim_copy(query.query_string());
        QString queryString = QString::fromStdString(query_string);

        qDebug() << "queryString: " << queryString;

        // Only push the departments when the query string is null
        if ( queryString.length() == 0 ) {
            qDebug() << "it is going to push the departments...!";
            pushDepartments( reply );
        }

        search(reply);

    } catch ( domain_error &e ) {
        // Handle exceptions being thrown by the client API
        cerr << e.what() << endl;
        reply->error( current_exception() );
    }
}

我们通过如下的方式向网路发出一个请求:

QByteArray Query::makeRequest(SearchReplyProxy const& reply,QNetworkRequest &request) const {
    int argc = 1;
    char *argv = const_cast<char*>("rss-scope");
    QCoreApplication *app = new QCoreApplication( argc, &argv );

    QNetworkAccessManager manager;
    QByteArray response;
    QNetworkDiskCache *cache = new QNetworkDiskCache();
    QString cachePath = g_scopePath + "/cache";
    //qDebug() << "Cache dir: " << cachePath;
    cache->setCacheDirectory(cachePath);

    request.setRawHeader( "User-Agent", g_userAgent.toStdString().c_str() );
    request.setRawHeader( "Content-Type", "application/rss+xml, text/xml" );
    request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );

    QObject::connect(&manager, SIGNAL(finished(QNetworkReply*)), app, SLOT(quit()));
    QObject::connect(&manager, &QNetworkAccessManager::finished,
                     [this, &reply, &response](QNetworkReply *msg) {
        if (msg->error() != QNetworkReply::NoError) {
            qCritical() << "Failed to get data: " << msg->error();
            pushError( reply, NO_CONNECTION );
        } else {
            response = msg->readAll();
        }

        msg->deleteLater();
    });

    manager.setCache( cache );
    manager.get( request );
    app->exec();

    delete cache;
    return response;
}

这个请求得到的数据将被“getMailInfo”使用并解析。最终被展示出来。我们希望对用户所输入的数据有所提示,所以我们对.ini文件做了如下的修改:

[ScopeConfig]
DisplayName = 快递查询
Description = This is a Mailcheck scope
Art = screenshot.png
Author = Firstname Lastname
Icon = icon.png
SearchHint = 请输入单号

[Appearance]
PageHeader.Logo = logo.png

这里的“SearchHint”显示的是提示的信息。

另外一个值得注意的是:当我们点击Scope中的每一项时,我们发现没有相应的Preview页面。这是通过修改“renderer”目录下的文件实现的,尽管在Desktop上是可以看到preview页面的。

{
    "schema-version": 1,
    "template": {
        "category-layout": "vertical-journal",
        "card-layout": "horizontal",
        "card-size": "medium",
        "collapsed-rows": 0,
	"non-interactive":"true"
    },
    "components": {
        "art": "image",
        "title": "title",
        "subtitle": "subtitle",
        "summary": "summary"
    }
}

我可以打开“data/render”目录下的“journal.json”。我们看见上面的“ non-interactive”项被置为“true”。这项表明它不希望有交互。

所有项目的源码在如下的地址可以找到:


bzr branch  lp:~liu-xiao-guo/debiantrial/mailcheckfinal