Canonical Voices

What Ubuntu Touch Development in CSDN (Chinese) talks about

UbuntuTouch

我们可以利用Ubuntu SDK中的GeocodeModel来在进行地理编码.比如说,我们给出一个地名,通过GeocodeModel来查询该地理位置的具体的信息,比如经度维度,街道信息等.在今天的文章中,我们通过一个简单的例程来展示如何查询一个地点.

就像我们的API文档中介绍的那样:

    GeocodeModel {
        id: geocodeModel
        plugin: plugin
        autoUpdate: false

        onStatusChanged: {
            mymodel.clear()
            console.log("onStatusChanged")
            if ( status == GeocodeModel.Ready ) {
                var count = geocodeModel.count
                console.log("count: " + geocodeModel.count)
                for ( var i = 0; i < count; i ++ ) {
                    var location = geocodeModel.get(i);
                    mymodel.append( {"location": location})
                }
            }
        }

        onLocationsChanged: {
            console.log("onStatusChanged")
        }

        Component.onCompleted: {
            query = "中国 北京 朝阳 望京"
            update()
        }
    }
在上面的代码中,当我们的GeocodeModel被装载后,我们发送一个查询的请求:

            query = "中国 北京 朝阳 望京"
            update()
当这个请求有结果回来后,在我们的onStatusChanged中返回我们所需要的结果.我们可以通过列表的方式显示我们所需要的结果.我们所有的代码为:

Main.qml


import QtQuick 2.4
import Ubuntu.Components 1.3
import QtLocation 5.3
import QtPositioning 5.2

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "geocodemodel.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)

    Plugin {
        id: plugin
        name: "osm"
    }

    ListModel {
        id: mymodel
    }

    PositionSource {
        id: me
        active: true
        updateInterval: 1000
        preferredPositioningMethods: PositionSource.AllPositioningMethods
        onPositionChanged: {
            console.log("lat: " + position.coordinate.latitude + " longitude: " +
                        position.coordinate.longitude);
            console.log(position.coordinate)
            console.log("mapzoom level: " + map.zoomLevel)
            map.coordinate = position.coordinate
        }

        onSourceErrorChanged: {
            console.log("Source error: " + sourceError);
        }
    }

    GeocodeModel {
        id: geocodeModel
        plugin: plugin
        autoUpdate: false

        onStatusChanged: {
            mymodel.clear()
            console.log("onStatusChanged")
            if ( status == GeocodeModel.Ready ) {
                var count = geocodeModel.count
                console.log("count: " + geocodeModel.count)
                for ( var i = 0; i < count; i ++ ) {
                    var location = geocodeModel.get(i);
                    mymodel.append( {"location": location})
                }
            }
        }

        onLocationsChanged: {
            console.log("onStatusChanged")
        }

        Component.onCompleted: {
            query = "中国 北京 朝阳 望京"
            update()
        }
    }

    Page {
        id: page
        header: standardHeader

        PageHeader {
            id: standardHeader
            visible: page.header === standardHeader
            title: "Geocoding"
            trailingActionBar.actions: [
                Action {
                    iconName: "edit"
                    text: "Edit"
                    onTriggered: page.header = editHeader
                }
            ]
        }

        PageHeader {
            id: editHeader
            visible: page.header === editHeader
            leadingActionBar.actions: [
                Action {
                    iconName: "back"
                    text: "Back"
                    onTriggered: {
                        page.header = standardHeader
                    }
                }
            ]
            contents: TextField {
                id: input
                anchors {
                    left: parent.left
                    right: parent.right
                    verticalCenter: parent.verticalCenter
                }
                placeholderText: "input words .."
                text: "中国 北京 朝阳 望京"

                onAccepted: {
                    geocodeModel.query = text
                    geocodeModel.update()
                }
            }
        }

        Item  {
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                top: page.header.bottom
            }

            Column {
                anchors.fill: parent

                ListView {
                    id: listview
                    clip: true
                    width: parent.width
                    height: parent.height/3
                    opacity: 0.5
                    model: mymodel
                    delegate: Item {
                        id: delegate
                        width: listview.width
                        height: layout.childrenRect.height + units.gu(0.5)

                        Column {
                            id: layout
                            width: parent.width

                            Text {
                                width: parent.width
                                text: location.address.text
                                wrapMode: Text.WordWrap
                            }

                            Text {
                                text: "(" + location.coordinate.longitude + ", " +
                                      location.coordinate.latitude + ")"
                            }

                            Rectangle {
                                width: parent.width
                                height: units.gu(0.1)
                                color: "green"
                            }
                        }

                        MouseArea {
                            anchors.fill: parent
                            onClicked: {
                                console.log("it is clicked")
                                map.coordinate = location.coordinate
                                // We do not need the position info any more
                                me.active = false
                            }
                        }
                    }
                }

                Map {
                    id: map
                    width: parent.width
                    height: parent.height*2/3
                    property var coordinate

                    plugin : Plugin {
                        name: "osm"
                    }

                    zoomLevel: 14
                    center: coordinate

                    MapCircle {
                        center: map.coordinate
                        radius: units.gu(3)
                        color: "red"
                    }

                    Component.onCompleted: {
                        zoomLevel = 14
                    }
                }
            }
        }

        Component.onCompleted: {
            console.log("geocodeModel limit: " + geocodeModel.limit)
        }
    }
}

运行我们的应用:

   
 

整个项目的源码:https://github.com/liu-xiao-guo/geocodemodel

作者:UbuntuTouch 发表于2016/6/7 13:54:14 原文链接
阅读:484 评论:0 查看评论

Read more
UbuntuTouch

在今天的文章中,我们将介绍如何利用Map API来展示如何显示我们的坐标信息,并展示如何在地图上标记我们的位置.我可以通过这个简单的应用显示在我们手机中所支持的Plugin的Providers(提供者).同时,我们也可以列出目前所支持的所有的MapType,并通过选择它们来展示我们的结果.


1)显示我们所有的Plugin的Service Providers



我们可以通过Plugin的API接口:

    Plugin {
        id: plugin

        // Set the default one
        Component.onCompleted: {
            name = availableServiceProviders[0]
        }
    }

来获取,我们目前所支持的所有的plugin.目前在我们的MX4手机上所展示的Providers:



就像上面的图中显示的那样,我们可以发现"nokia"及"osm"两个service providers.目前的情况是在中国"nokia"是用不了的(这可能是由于license的原因).在国外,应该可以使用NokiaHere地图的.

2)如何在地图中标注


简单地,我们可以使用MapCircle

MapCircle {
    center: me.position.coordinate
    radius: units.gu(3)
    color: "red"
}

在我们的地图中用一个全来标注我们所感兴趣的点.在这里,"center"用来表示圆圈的中心坐标位置.
我们可以MapQuickItem来用任何一个Item来标注我们的兴趣点.在我们的例程中,我们使用了如下的方法:

                    MapQuickItem {
                        id: mylocation
                        sourceItem: Item {
                            width: units.gu(6)
                            height: info.height

                            Label {
                                id: info
                                anchors.centerIn: parent
                                anchors.verticalCenterOffset: -units.gu(2)
                                text: "(" + me.position.coordinate.longitude.toFixed(2) + "," + me.position.coordinate.latitude.toFixed(2) + ")"
                                color: "blue"
                            }


                            Rectangle {
                                width: units.gu(2)
                                height: width
                                radius: width/2
                                color: "red"
                                x: parent.width/2
                                y: parent.height/2
                            }
                        }

我们使用了一个圆点及一个可以显示坐标的Label.



我们可以在代码中动态地生产我们需要的QML Component并标注我们的位置信息:

           MouseArea {
                        anchors.fill: parent

                        onPressed: {
                            if ( setMarks.checked ===false ) {
                                mouse.accepted = false
                                return;
                            }

                            console.log("mouse: " + mouseX + " " + mouseY)
                            var coord = map.toCoordinate(Qt.point(mouseX, mouseY))

                            console.log("creating the component")
                            var component = Qt.createComponent("MapMarkItem.qml")
                            console.log("creating the item")
                            var item = component.createObject(map, { coordinate: coord })
                            console.log("adding the item")
                            map.addMapItem(item)

//                            var circle = Qt.createQmlObject('import QtLocation 5.3; MapCircle {}', map)
//                            circle.center = coord
//                            circle.radius = units.gu(4)
//                            circle.color = 'green'
//                            circle.border.width = 3
//                            map.addMapItem(circle)

                            mouse.accepted = true;
                        }
                    }


在上面我们通过Qt.createQmlObject来动态地创建一个MapCirle的控件.我们通过map.addMapItem来添加进去.当我们点击地图时,我们把标注加入到地图中去:



我们可以获得任何一个地点的位置信息.这在中国的手机地图中是得不到这个信息的:)

3)如何通过手势操作放大/缩小/移动地图



在默认的情况下,我们可以直接通过手势的操作来进行Zoom及Pan我们的地图.我们也可以定义我们自己的gesture来启动或禁止这个手势的操控:

                    gesture {
                        enabled: !setMarks.checked
                        activeGestures: MapGestureArea.ZoomGesture | MapGestureArea.PanGesture

                        onPanStarted:  {
                            console.log("onPanStarted")
                        }

                        onPanFinished: {
                            console.log("onPanFinished")
                        }

                        onPinchStarted: {
                            console.log("onPinchStarted")
                        }

                        onPinchFinished: {
                            console.log("onPinchFinished")
                        }

                        onPinchUpdated: {
                            console.log("onPinchUpdated")
                            console.log("point1: " + "(" + pinch.point1.x + pinch.point1.y + ")")
                        }
                    }

当我们的enabled项设为false时,我们可以进行任何的zoom或pan.


4)如何获得所有的MapType



我们可以通过设置Map的MapType来查看卫星,夜间,地形图等信息.我们可以通过如下的API来列表所有的MapType:

            leadingActionBar {
                id: leadbar
                actions: {
                    var supported = map.supportedMapTypes;
                    console.log("count: " + supported.length)
                    var acts = []
                    console.log("Going to add the types")
                    for ( var i = 0; i < supported.length; i++ ) {
                        var item = supported[i]

                        console.log("map type name: " + item.name)
                        console.log("map style: " + item.style)
                        console.log("type des:" + item.description)
                        var action = creatAction(leadbar, "info", item)
                        acts.push(action)
                    }

                    return acts
                }
            }

在这里,我们使用了"map.supportedMapTypes"来列表所有的被支持的地图形式.最终在我们的应用中:


  

  


5)如何获得当前位置的位置信息



我们可以通过PositionSource接口获取我们的当前的位置信息:

    PositionSource {
        id: me
        active: true
        updateInterval: 1000
        preferredPositioningMethods: PositionSource.AllPositioningMethods
        onPositionChanged: {
            console.log("lat: " + position.coordinate.latitude + " longitude: " +
                        position.coordinate.longitude);
            console.log(position.coordinate)
            console.log("mapzoom level: " + map.zoomLevel)
        }

        onSourceErrorChanged: {
            console.log("Source error: " + sourceError);
        }
    }

我们可以通过设置updateInterval来得到获取位置的频率.

整个项目的源码在:https://github.com/liu-xiao-guo/gps




作者:UbuntuTouch 发表于2016/6/2 13:36:31 原文链接
阅读:507 评论:0 查看评论

Read more
UbuntuTouch

在今天的练习中,我们来做一个设计.在我们的ListView的列表中,我们想点击它的项时,它的项能够展开.这对于我们的有些设计是非常用的.比如我们不希望打开另外一个页面,但是我们可以展示我们当前项的更多的信息.我们可以使用Ubuntu SDK提供的Expandable.这个设计的图片为:


 


如果每个项的详细信息并不多的时候,我们可以利用这种方法来展示我们的每个项的内容.具体的代码为:


Main.qml


import QtQuick 2.4
import Ubuntu.Components 1.3
import Ubuntu.Components.ListItems 1.3 as ListItem

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "expandable.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)

    ListModel {
        id: listmodel
        ListElement { name: "image1.jpg" }
        ListElement { name: "image2.jpg" }
        ListElement { name: "image3.jpg" }
        ListElement { name: "image4.jpg" }
        ListElement { name: "image5.jpg" }
        ListElement { name: "image6.jpg" }
        ListElement { name: "image7.jpg" }
        ListElement { name: "image8.jpg" }
        ListElement { name: "image9.jpg" }
        ListElement { name: "image10.jpg" }
        ListElement { name: "image11.jpg" }
    }

    Page {
        header: PageHeader {
            id: pageHeader
            title: i18n.tr("expandable")
        }

        Item {
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                top: pageHeader.bottom
            }

            UbuntuListView {
                id: listview
                anchors.fill: parent
                height: units.gu(24)
                model: listmodel
                delegate: ListItem.Expandable {
                    id: exp
                    expandedHeight: units.gu(15)
                    expanded: listview.currentIndex == index

                    Row {
                        id: top
                        height: collapsedHeight
                        spacing: units.gu(2)
                        Image {
                            height: parent.height
                            width: height
                            source: "images/" + name
                        }

                        Label {
                            text: "This is the text on the right"
                        }
                    }

                    Label {
                        anchors.top: top.bottom
                        anchors.topMargin: units.gu(0.5)
                        text: "This is the detail"
                    }

                    onClicked: {
//                        expanded = true;
                        listview.currentIndex = index
                    }
                }
            }
        }

    }
}



作者:UbuntuTouch 发表于2016/6/15 17:31:13 原文链接
阅读:356 评论:0 查看评论

Read more
UbuntuTouch

在我们先前的文章"如何在QML应用中创建一个Context Menu",我们使用了一个popup的方法来显示一个我们自己需要的context menu.那个方法虽好,但是显示起来和背景的颜色很相近,不容易看出来.当然我们可以通过一些方法来改进我的设计.在今天的例程中,我们通过另外一种方法来做一个同样的context menu.这个方法的好处是,我们可以随意来设计我们所需要的效果.我们从另外一个角度来实现同样的东西.我们设计的最终效果为:


    


我们可以通点击上图中的图标来弹出我们所需要的选项,在选项中,我们可以做出我们的选择.当选择完后,我们可以点击图标收回选项.基于这个设计我们还可以更进一步做更多的改进.比如点击区域以外的地方自动让选项列表消失.当然,我们也可以加入我们的动画效果.


上面图中的一个图标及一个文字,我们是通过一个AbstractButton控件来实现的.当然,我们也可以采用一个ListView来实现.


OptionValueButton.qml


import QtQuick 2.4
import Ubuntu.Components 1.3

AbstractButton {
    id: optionValueButton

    implicitHeight: units.gu(5)

    property alias label: label.text
    property alias iconName: icon.name
    property bool selected
    property bool isLast
    property int columnWidth
    property int marginSize: units.gu(1)

    width: marginSize + iconLabelGroup.width + marginSize

    Item {
        id: iconLabelGroup
        width: childrenRect.width
        height: icon.height

        anchors {
            left: (iconName) ? undefined : parent.left
            leftMargin: (iconName) ? undefined : marginSize
            horizontalCenter: (iconName) ? parent.horizontalCenter : undefined
            verticalCenter: parent.verticalCenter
            topMargin: marginSize
            bottomMargin: marginSize
        }

        Icon {
            id: icon
            anchors {
                verticalCenter: parent.verticalCenter
                left: parent.left
            }
            width: optionValueButton.height - optionValueButton.marginSize * 2
            color: "white"
            opacity: optionValueButton.selected ? 1.0 : 0.5
            visible: name !== ""
        }

        Label {
            id: label
            anchors {
                left: icon.name != "" ? icon.right : parent.left
                verticalCenter: parent.verticalCenter
                leftMargin: units.gu(0.5)
            }

            color: "white"
            opacity: optionValueButton.selected ? 1.0 : 0.5
            width: paintedWidth
        }
    }

    Rectangle {
        anchors {
            left: parent.left
            bottom: parent.bottom
        }
        width: parent.columnWidth
        height: units.dp(1)
        color: "red"
        opacity: 0.5
        visible: true
    }
}

我们的Main.qml设计也比较简单.只是在这里面,我们使用了Item中mapFromItem来得到我们点击的控件的位置信息.利用这个位置信息,我们来定位我们的选项的optionValueSelector位置.整个设计如下:

Main.qml


import QtQuick 2.4
import Ubuntu.Components 1.3

/*!
    \brief MainView with a Label and Button elements.
*/

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "mapfromitem.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)
    theme.name :"Ubuntu.Components.Themes.SuruDark"
    property bool optionValueSelectorVisible: false

    Page {
        header: PageHeader {
            id: pageHeader
            title: i18n.tr("mapfromitem")
        }

        ListModel {
            id: model
            property int selectedIndex: 0

            ListElement {
                icon: "account"
                label: "On"
                value: "flash-on"
            }
            ListElement {
                icon: "active-call"
                label: "Auto"
                value: "Auto"
            }
            ListElement {
                icon: "call-end"
                label: "Off"
                value: "flash-off"
            }
        }

        Column {
            id: optionValueSelector
            objectName: "optionValueSelector"
            width: childrenRect.width
            spacing: units.gu(1)

            property Item caller

            function toggle(model, callerButton) {
                if (optionValueSelectorVisible && optionsRepeater.model === model) {
                    hide();
                } else {
                    show(model, callerButton);
                }
            }

            function show(model, callerButton) {
                optionValueSelector.caller = callerButton;
                optionsRepeater.model = model;
                alignWith(callerButton);
                optionValueSelectorVisible = true;
            }

            function hide() {
                optionValueSelectorVisible = false;
                optionValueSelector.caller = null;
            }

            function alignWith(item) {
                // horizontally center optionValueSelector with the center of item
                // if there is enough space to do so, that is as long as optionValueSelector
                // does not get cropped by the edge of the screen
                var itemX = parent.mapFromItem(item, 0, 0).x;
                var centeredX = itemX + item.width / 2.0 - width / 2.0;
                var margin = units.gu(1);

                if (centeredX < margin) {
                    x = itemX;
                } else if (centeredX + width > item.parent.width - margin) {
                    x = itemX + item.width - width;
                } else {
                    x = centeredX;
                }

                // vertically position the options above the caller button
                y = Qt.binding(function() { return item.y - height - units.gu(2) });

                console.log("x: " + x + " y: " + y)
            }

            visible: opacity !== 0.0
            onVisibleChanged: if (!visible) optionsRepeater.model = null;
            opacity: optionValueSelectorVisible ? 1.0 : 0.0
            Behavior on opacity {UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}}

            Repeater {
                id: optionsRepeater

                delegate: OptionValueButton {
                    anchors.left: optionValueSelector.left
                    columnWidth: optionValueSelector.childrenRect.width
                    label: model.label
                    iconName: model.icon
                    selected: optionsRepeater.model.selectedIndex == index
                    isLast: index === optionsRepeater.count - 1
                    onClicked: {
                        optionsRepeater.model.selectedIndex = index
                        console.log(optionsRepeater.model.get(index).value)
                    }
                }
            }
        }

        Icon {
            id: optionButton
            width: units.gu(3)
            height: width
            anchors.centerIn: parent
            name: model.get(model.selectedIndex).icon

            MouseArea {
                anchors.fill: parent
                onClicked: {
                    console.log("optionValueSelectorVisible: " + optionValueSelectorVisible)
                    optionValueSelector.toggle(model, optionButton)
                }
            }
        }

        Component.onCompleted: {
            console.log("width: " + width + " height: " +height)
        }
    }

}

在我们的实际应用中,我们可以把它设计为Component供我们其它的应用直接使用.我们可以通过InverseMouseArea来更进一步完成整个Component的可用性.




作者:UbuntuTouch 发表于2016/5/25 10:07:36 原文链接
阅读:374 评论:0 查看评论

Read more
UbuntuTouch

针对Scope的设计,有些时候我们不想,或并没有必要切换到我们的preview界面.比如,我们设计一个汇率转换Scope,在query界面中直接显示汇率的结果即可.并没有必要切换到我们的preview界面.针对我之前设计的"在Ubuntu平台上开发快递邮件查询Scope",我们也没有设计自己的preview界面.直接在主界面中输出自己的结果即可.那么我们如何来实现这个目的呢?


 


在我们的设计中,我们必须在我们的template中:


const static string CAT_RENDERER102
{
    R"(
    {
        "schema_version" : 1,
        "template" :
        {
            "category-layout" : "grid",
            "card-layout": "vertical",
            "card-size" : "large",
            "card-background": "#00FF00",
            "overlay": true,
            "non-interactive":"true"
        },
        "components" :
        {
            "title" : "title",
            "art" : "art",
            "subtitle": "subtitle",
            "mascot": "mascot",
            "emblem": "emblem",
            "summary": "summary",
            "overlay-color": "overlay-color",
            "attributes": {
                "field": "attributes",
                "max-count": 2
            }
        }
    }
    )"
};

加入non-interactive":"true"这么一项.它表明,我们不需要交互,也就是只能显示第一个query的界面.我们还是用我们的scopetemplate例程来展示:




就如上面显示的那样.点击上面的图片,它不会带我们去到preivew的页面.

整个项目的源码在:https://github.com/liu-xiao-guo/scopetemplates_buttons


作者:UbuntuTouch 发表于2016/5/26 17:18:47 原文链接
阅读:343 评论:0 查看评论

Read more
UbuntuTouch

[原]如何在Snap包中定义全局的plug

我们知道在我们snap应用中,我们可以通过定义plug来访问我们所需要的资源.在一个snap包中,我们也可以定义许多的应用,每个应用可以分别定义自己的plug.假如一个Snap包有一个plug是共同的,那么,我们有上面办法来定义一个全局的plug,这样所有的在同一个包中的所有的snap应用都可以同时拥有这个plug.这个到底怎么做呢?

关于snap中的interface及plug概念,请参阅我之前的文章"安装snap应用到Ubuntu 16.4桌面系统".

最近,我读了一篇关于snap interface的文章.很受启发.文章可以在地址找到.其中有一段非常有意思的一段话:


Note that only the links app refers to plugs, the bookmarks app does not. If a plug or slot is declared in a snap, but not associated with a specific application they will be implicitly bound to all apps. When a plug or slot is specified by one or more apps, as in the above example, it will be bound only to those applications. Compare that to the following code:

它的意思就是如果我们在我们的snapcraft.yaml中定义一个plug,但是它不被任何的应用所应用,那么它隐含地就是所有的应用都有这个plug.


我们拿我们的例程https://github.com/liu-xiao-guo/helloworld-plug为例,我们定义如下:


name: hello-xiaoguo
version: "1.0"
architectures: [ all ]
summary: The 'hello-world' of snaps
description: |
    This is a simple snap example that includes a few interesting binaries
    to demonstrate snaps and their confinement.
    * hello-world.env  - dump the env of commands run inside app sandbox
    * hello-world.evil - show how snappy sandboxes binaries
    * hello-world.sh   - enter interactive shell that runs in app sandbox
    * hello-world      - simply output text
confinement: strict
type: app  #it can be gadget or framework

apps:
 env:
   command: bin/env
 evil:
   command: bin/evil
 sh:
   command: bin/sh
 hello-world:
   command: bin/echo
 createfile:
   command: bin/createfile
 createfiletohome:
   command: bin/createfiletohome
 writetocommon:
   command: bin/writetocommon

plugs:
    home:
        interface: home

parts:
 hello:
  plugin: copy
  files:
    ./bin: bin

在上面的snapcraft.yaml文件中,我们写了如下的句子:

plugs:
    home:
        interface: home

由于在我们的任何一个应用中都没有引用home plug,所有这个plug将被定义为包级的plug,也就是说所有的app都享有这个plug.我们可以做一个简单的测试.我们安装好我们的hello snap后,执行如下的命令:

liuxg@liuxg:~$ hello-xiaoguo.createfiletohome 
Hello a nice World!
This example demonstrates the app confinement
This app tries to write to its own user directory
Succeeded! Please find a file created at /home/liuxg/snap/hello-xiaoguo/x1/test.txt
If do not see this, please file a bug


我们的createtohome脚本如下:

#!/bin/sh

set -e
echo "Hello a nice World!"

echo "This example demonstrates the app confinement"
echo "This app tries to write to its own user directory"

echo "Haha" > /home/$USER/test.txt

echo "Succeeded! Please find a file created at $HOME/test.txt"
echo "If do not see this, please file a bug"

显然它向home里写入一个叫做test.txt的文件.我们的写入操作是成功的.


从我们的文件目录中,我们可以看出来我们刚刚创建的文件test.txt.细心的开发者也可以出去上面定义的plug,在试一试是否成功?







作者:UbuntuTouch 发表于2016/8/19 17:07:51 原文链接
阅读:20 评论:0 查看评论

Read more
UbuntuTouch

在最新的QtMultiMedia 5.6中,AudioAPI中有一个playlist属性.我们可以充分利用这个属性来做一个简单的音乐播放器.在我们的官方文档中,写的是QttMultiMedia 5.4.正确的是5.6版本.Audio API可以让我们很方便地播放我们所需要的音乐.在我们之前的文章"如何在Ubuntu QML应用中播放音乐"也讨论过如何利用MediaPlayerSoundEffect来播放音乐.


如何利用Audio 中的playlist呢?我们先来看一个简单的例子:


        Audio {
            id: player;
            autoPlay: true
            autoLoad: true
            playlist: Playlist {
                id: playlist
            }

            Component.onCompleted: {
                console.log(playlist.addItem(Qt.resolvedUrl("sounds/song1.mp3")))
                console.log(playlist.addItem(Qt.resolvedUrl("sounds/background.mp3")))
                console.log("playlist count: " + playlist.itemCount)
                console.log("metaData type: " + typeof(meta))

                play()
            }
        }

在上面的代码中,我们向我们的playlist表中添加我们的音乐文件.当然,我们也可以动态地添加它们到我们的列表中.我们也可以通过如下的方式来添加我们的音乐列表:


    Audio {
        id: player;
        playlist: Playlist {
            id: playlist
            PlaylistItem { source: "song1.ogg"; }
            PlaylistItem { source: "song2.ogg"; }
            PlaylistItem { source: "song3.ogg"; }
        }
    }
    ListView {
        model: playlist;
        delegate: Text {
            font.pixelSize: 16;
            text: source;
        }
    }

目前在Ubuntu手机上测试.这种方法有一个bug.如果音乐文件存在的话,那么在列表中将会有两个相同的项被显示.目前,我们还是使用前面介绍的方法来做我们的例程.


Main.qml

import QtQuick 2.4
import Ubuntu.Components 1.3
import QtMultimedia 5.6

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "playlist.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)
    property var meta: player.metaData

    Page {
        id: page
        header: PageHeader {
            id: pageHeader
            title: i18n.tr("playlist")
        }

        Audio {
            id: player;
            autoPlay: true
            autoLoad: true
            playlist: Playlist {
                id: playlist
            }

            Component.onCompleted: {
                console.log(playlist.addItem(Qt.resolvedUrl("sounds/song1.mp3")))
                console.log(playlist.addItem(Qt.resolvedUrl("sounds/background.mp3")))
                console.log("playlist count: " + playlist.itemCount)
                console.log("metaData type: " + typeof(meta))

                console.log("The properties of metaData is:")
                var keys = Object.keys(meta);
                for( var i = 0; i < keys.length; i++ ) {
                    var key = keys[ i ];
                    var data = key + ' : ' + meta[ key ];
                    console.log( key + ": " + data)
                }

                play()
            }
        }

        Flickable {
            anchors {
                left: parent.left
                right: parent.right
                top: pageHeader.bottom
                bottom: parent.bottom
            }
            contentHeight: layout.childrenRect.height +
                           layout1.height + layout1.spacing

            Column {
                id: layout
                anchors.fill: parent

                ListView {
                    anchors.left: parent.left
                    anchors.right: parent.right
                    height: page.height/2

                    model: playlist;
                    delegate: Label {
                        fontSize: "x-large"
                        text: {
                            var filename = String(source);
                            var name = filename.split("/").pop();
                            return name;
                        }

                        MouseArea {
                            anchors.fill: parent

                            onClicked: {
                                if (player.playbackState != Audio.PlayingState) {
                                    player.playlist.currentIndex = index;
                                    player.play();
                                } else {
                                    player.pause();
                                }
                            }
                        }
                    }
                }

                Rectangle {
                    width: parent.width
                    height: units.gu(0.1)
                    color: "red"
                }

                Slider {
                    id: defaultSlider
                    anchors.horizontalCenter: parent.horizontalCenter
                    width: parent.width * 0.8
                    maximumValue: 100
                    value: player.position/player.duration * maximumValue
                }

                CustomListItem {
                    title.text:  {
                        switch (player.availability ) {
                        case Audio.Available:
                            return "availability: available";
                        case Audio.Busy:
                            return "availability: Busy";
                        case Audio.Unavailable:
                            return "availability: Unavailable";
                        case Audio.ResourceMissing:
                            return "availability: ResourceMissing";
                        default:
                            return "";
                        }
                    }
                }

                CustomListItem {
                    title.text: "bufferProgress: " + player.bufferProgress;
                }

                CustomListItem {
                    title.text: "duration: " + player.duration/1000 + " sec"
                }

                CustomListItem {
                    title.text: "hasAudio: " + player.hasAudio
                }

                CustomListItem {
                    title.text: "hasVideo: " + player.hasVideo
                }

                CustomListItem {
                    title.text: "loops: " + player.loops
                }

                CustomListItem {
                    title.text: "muted: " + player.muted
                }

                CustomListItem {
                    title.text: "playbackRate: " + player.playbackRate
                }

                CustomListItem {
                    title.text: {
                        switch (player.playbackState) {
                        case Audio.PlayingState:
                            return "playbackState : PlayingState"
                        case Audio.PausedState:
                            return "playbackState : PausedState"
                        case Audio.StoppedState:
                            return "playbackState : StoppedState"
                        default:
                            return ""
                        }
                    }
                }

                CustomListItem {
                    title.text: "seekable: " + player.seekable
                }

                CustomListItem {
                    title.text: "url: " + String(player.source)
                }

                CustomListItem {
                    title.text: "volume: " + player.volume
                }

                CustomListItem {
                    title.text: {
                        switch (player.status) {
                        case Audio.NoMedia:
                            return "status: NoMedia"
                        case Audio.Loading:
                            return "status: Loading"
                        case Audio.Loaded:
                            return "status: Loaded"
                        case Audio.Buffering:
                            return "status: Buffering"
                        case Audio.Stalled:
                            return "status: Stalled"
                        case Audio.Buffered:
                            return "status: Buffered"
                        case Audio.EndOfMedia:
                            return "status: EndOfMedia"
                        case Audio.InvalidMedia:
                            return "status: InvalidMedia"
                        case Audio.UnknownStatus:
                            return "status: UnknownStatus"
                        default:
                            return ""
                        }
                    }
                }

            }
        }

        Row {
            id: layout1
            anchors.bottom: parent.bottom
            anchors.bottomMargin: units.gu(1)
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: units.gu(5)

            Button {
                text: "Previous"
                onClicked: {
                    console.log("Previouse is clicked")
                    var previousIndex = player.playlist.previousIndex(1)
                    console.log("previousIndex: " + player.playlist.previousIndex(1))
                    if ( previousIndex == -1 ) {
                        player.playlist.currentIndex = player.playlist.itemCount - 1;
                    } else {
                        player.playlist.previous();
                    }

                    player.play()
                }
            }

            Button {
                text: "Next"
                onClicked: {
                    console.log("Next is clicked")
                    var nextIndex = player.playlist.nextIndex(1)
                    console.log("nextIndex: " + nextIndex )
                    if (nextIndex == -1) {
                        player.playlist.currentIndex = 0
                    } else {
                        player.playlist.next();
                    }

                    player.play();
                }
            }
        }
    }
}

整个的设计还是非常简单.屏幕的上面是一个音乐的列表.下面是一个该音乐文件的状态.由于一些原因,我们目前还不能得到音乐文件的metaData.

   


我们可以通过点击列表中的像来播放或停止正在播放的音乐.





作者:UbuntuTouch 发表于2016/5/27 12:18:52 原文链接
阅读:409 评论:4 查看评论

Read more
UbuntuTouch

在我们的Ubuntu SDK中,我们提供了QtOrganizer API接口.通过这个API我们可以来管理我们自己的日历,也可以利用这个API来导入一个日历.在今天的例程中,我们来详细了解这个API的具体的用法.在练习的开始,我们可以在我们的手机中的"日历"应用中创建一些我们所需要的事件.比如:

  



1)如何得到手机中所有的manager 


我们可以通过这个OrganizerModel API来列举在我们的系统中所支持的所有的managers.

availableManagers : list<string>

这里的每个manager对应于一个数据库.这样我们就可以对它们进行操作.在目前的MX4手机中,我们列出的manager为:



在上面列出了三个manager: invalid,memory及eds.这里的memory可以被用在来导入我们在文件中的一个文件,并最终形成我们可以对一个文件中的事件进行操作.当然,我们也可以实现同步的功能,或导出的动作.具体的例程可以参阅文章"Qt Quick Organizer List View Example".虽然例程中使用的API的有些接口和我们现有的会有所不同,但是我们可以实现同样的功能.在上面图中显示的"eds"就是我们的手机中自动应用"日历"所使用的manager.在接下来的操作中,我们主要来通过这个来进行剖析.


2)如何得到所有的event


我们可以通过OrganizerModel中的如下方法来得到所有的event的列表:

items : list<OrganizerItem>

通过这个API,我们可以得到在我们的日历中所有的事件的列表:



就如上图所示的那样,我们可以得到所有的事件的列表信息.


3)如何得到每个事件的详细信息



我们可以通过OrganizerItem来得到每个事件的具体信息.我们可以通过:

itemDetails : list<Detail>

来得到事件已经拥有的那些属性.我们可以通过如下的API来得到该项的所有的具体的信息:

Detail detail(type)

比如在上面图中显示的时间信息.对于一个事件来说:



从上面的图中可以看出来,每个事件的具体信息,并展示它所支持的具体的属性项目.我们可以尝试点击最下面的按钮"Change the description to I LOVE"来改变我们的事件的description. 修改过后的列表为:




4)如何删除一个事件


我们可以通过OrganizerModel中的:

removeItem(OrganizerItem item)

来删除我们不想要的事件.在我们的应用中,我们可以通过向左滑动,显示出删除图标.点击删除图标即可达到删除的目的.

  

可以看出来,我们已经成功地删除了我们的"day event on June 8th".

当然我们可以更多地来explore更过的关于QtOrganizer的API接口.这个练习就留给开发者自己来做.在这里我们就只抛砖引玉地展示其中的一部分.

Main.qml


import QtQuick 2.4
import Ubuntu.Components 1.3
import QtOrganizer 5.0

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "organizer.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)

    OrganizerModel {
        id: organizerModel

        endPeriod: {
            var date = new Date();
            date.setDate(date.getDate() + 10);
            return date
        }

        sortOrders: [
            SortOrder {
                id: sortOrder

                detail: Detail.EventTime
                field: EventTime.startDateTime
                direction: Qt
            }
        ]

        onExportCompleted: {
            console.log("onExportCompleted")
        }

        onImportCompleted: {
            console.log("onImportCompleted")
        }

        onItemsFetched: {
            console.log("onItemsFetched")
        }

        onModelChanged: {
            console.log("onModelChanged")
            console.log("item count: " + organizerModel.itemCount)
            mymodel.clear();
            var count = organizerModel.itemCount
            for ( var i = 0; i < count; i ++ ) {
                var item = organizerModel.items[i];
                mymodel.append( {"item": item })
            }
        }

        onDataChanged: {
            console.log("onDataChanged")
        }

        manager: "eds"
    }

    ListModel {
        id: mymodel
    }

    Component {
        id: highlight
        Rectangle {
            width: parent.width
            color: "lightsteelblue"; radius: 5
            Behavior on y {
                SpringAnimation {
                    spring: 3
                    damping: 0.2
                }
            }
        }
    }

    PageStack {
        id: pagestack
        anchors.fill: parent

        Component.onCompleted: {
            pagestack.push(main)
        }

        Page {
            id: main
            header: PageHeader {
                id: pageHeader
                title: i18n.tr("organizer")
            }
            visible: false

            Item {
                anchors {
                    left: parent.left
                    right: parent.right
                    bottom: parent.bottom
                    top: pageHeader.bottom
                }

                Column {
                    anchors.fill: parent
                    spacing: units.gu(1)

                    Label {
                        text: "Organizer managers:"
                    }

                    ListView {
                        id: listview
                        width: parent.width
                        height: units.gu(10)
                        highlight: highlight
                        model:organizerModel.availableManagers
                        delegate: Label {
                            width: listview.width
                            text: modelData
                            fontSize: "large"

                            MouseArea {
                                anchors.fill: parent
                                onClicked: {
                                    listview.currentIndex = index
                                }
                            }
                        }
                    }

                    Rectangle {
                        id: divider
                        width: parent.width
                        height: units.gu(0.1)
                        color: "green"
                    }

                    ListView {
                        clip: true
                        width: parent.width
                        height: parent.height - divider.height - listview.height
                        model: mymodel
                        delegate: ListItem {
                            Label {
                                text: item.description
                            }

                            Label {
                                anchors.right: parent.right
                                text : {
                                    var evt_time = item.detail(Detail.EventTime)
                                    var starttime = evt_time.startDateTime;
                                    console.log("time: " + starttime.toLocaleDateString())
                                    return starttime.toLocaleDateString()
                                }
                            }

                            onClicked: {
                                detailpage.myitem = item
                                pagestack.push(detailpage)
                            }

                            trailingActions: ListItemActions {
                                actions: [
                                    Action {
                                        iconName: "delete"
                                        onTriggered: {
                                            console.log("delete is triggered!");
                                            organizerModel.removeItem(item)
                                        }
                                    }
                                ]
                            }
                        }
                    }
                }
            }

            Component.onCompleted: {
                console.log("manage name: " + organizerModel.manager)
            }
        }

        Page {
            id: detailpage
            header: PageHeader {
                id: header
                title: i18n.tr("Detail page")
            }
            visible: false

            property var myitem

            Item {
                anchors {
                    left: parent.left
                    right: parent.right
                    bottom: parent.bottom
                    top: header.bottom
                }

                Column {
                    anchors.fill: parent
                    spacing: units.gu(1)

                    Label {
                        id: col1
                        width: parent.width
                        text: "collectionId: " + detailpage.myitem.collectionId
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col2
                        width: parent.width
                        text: "description: " + detailpage.myitem.description
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col3
                        width: parent.width
                        text: "displayLabel: " + detailpage.myitem.displayLabel
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col4
                        width: parent.width
                        text: "guid: " + detailpage.myitem.guid
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col5
                        width: parent.width
                        text: "itemId: " + detailpage.myitem.itemId
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col6
                        width: parent.width
                        text: "itemType : " + detailpage.myitem.itemType
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col7
                        width: parent.width
                        text: "manager : " + detailpage.myitem.manager
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col8
                        width: parent.width
                        text: "modified  : " + detailpage.myitem.modified
                        wrapMode: Text.WordWrap
                    }

                    Label {
                        id: col9
                        width: parent.width
                        text: "Item details are:"
                    }

                    ListView {
                        width: parent.width
                        height: parent.height - col1.height - col2.height -col3.height - col9.height -
                                col4.height - col5.height - col6.height - col7.height - col8.height
                        model: detailpage.myitem.itemDetails
                        delegate: Label {
                            text: {
                                switch (modelData.type) {
                                case Detail.Undefined:
                                    return "Undefined";
                                case Detail.Classification:
                                    return "Classification"
                                case Detail.Comment:
                                    return "Comment";
                                case Detail.Description:
                                    return "Description"
                                case Detail.DisplayLabel:
                                    return "DisplayLabel"
                                case Detail.ItemType:
                                    return "ItemType"
                                case Detail.Guid:
                                    return "Guid"
                                case Detail.Location:
                                    return "Location"
                                case Detail.Parent:
                                    return "Parent"
                                case Detail.Priority:
                                    return "Priority"
                                case Detail.Recurrence:
                                    return "Recurrence"
                                case Detail.Tag:
                                    return "Tag"
                                case Detail.Timestamp:
                                    return "Timestamp"
                                case Detail.Version:
                                    return "Version"
                                case Detail.Reminder:
                                    return "Reminder"
                                case Detail.AudibleReminder:
                                    return "AudibleReminder"
                                case Detail.EmailReminder:
                                    return "EmailReminder"
                                case Detail.VisualReminder:
                                    return "VisualReminder"
                                case Detail.ExtendedDetail:
                                    return "ExtendedDetail"
                                case Detail.EventAttendee:
                                    return "EventAttendee"
                                case Detail.EventRsvp:
                                    return "EventRsvp"
                                case Detail.EventTime:
                                    return "EventTime"
                                case Detail.JournalTime:
                                    return "JournalTime"
                                case Detail.TodoTime:
                                    return "TodoTime"
                                case Detail.TodoProgress:
                                    return "TodoProgress"
                                default:
                                    return "Unknown type"
                                }
                            }
                        }
                    }
                }

                Button {
                    anchors.bottom: parent.bottom
                    anchors.bottomMargin: units.gu(1)
                    anchors.horizontalCenter: parent.horizontalCenter

                    text: "Change the description to I LOVE"
                    onClicked: {
                        console.log("changing the description")
                        detailpage.myitem.description = "I LOVE YOU"
                        organizerModel.saveItem(detailpage.myitem)
                    }
                }
            }

        }
    }
}







作者:UbuntuTouch 发表于2016/6/6 10:01:30 原文链接
阅读:273 评论:0 查看评论

Read more
UbuntuTouch

在我们先前的文章"如何在Ubuntu手机中使用前置照相机"对如何使用前置照相机给出了一个解决方案.事实上,在我们最新的Camera API中 (QtMultiMedia 5.6),已经有新的API来完成这个功能了.我们不再需要额外的C++代码来完成这个功能.


在新的Camera API中,我们可以指定:

deviceId : string
来使得我们的Camera来选择我们需要的前后相机.那么我们如何得到这个deviceId呢?在新的QtMultiMedia的接口中,我们可以通过如下的API来得到所有的Camera的描述信息:

QtMultimedia.availableCameras

通过这个API,我们可以得到一个Camera的如下信息:


基于上面的信息,我们做了如下的一个简单的照相机应用:

Main.qml


import QtQuick 2.4
import Ubuntu.Components 1.3
import QtMultimedia 5.6

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "cameralist.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)

    Camera {
        id: camera

        imageProcessing.whiteBalanceMode: CameraImageProcessing.WhiteBalanceFlash

        exposure {
            exposureCompensation: -1.0
            exposureMode: Camera.ExposurePortrait
        }

        flash.mode: Camera.FlashRedEyeReduction

        imageCapture {
            id: capture
            onImageCaptured: {
                console.log("onImageCaptured!")
                // Show the preview in an Image
                photoPreview.source = preview
            }

            onImageSaved: {
                console.log("image has been saved: " + requestId);
                console.log("saved path: " + path);
            }
        }
    }

    Component {
        id: highlight
        Rectangle {
            width: parent.width
            height: listview.delegate.height
            color: "lightsteelblue"; radius: 5
            Behavior on y {
                SpringAnimation {
                    spring: 3
                    damping: 0.2
                }
            }
        }
    }

    Page {
        id: page
        header: PageHeader {
            id: pageHeader
            title: i18n.tr("cameralist")
        }

        Item {
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                top: pageHeader.bottom
            }

            Column {
                anchors.fill: parent

                ListView {
                    id: listview
                    width: page.width
                    height: units.gu(20)

                    model: QtMultimedia.availableCameras
                    highlight:highlight
                    delegate: Item {
                        id: delegate
                        width: listview.width
                        height: layout.childrenRect.height + units.gu(0.5)

                        Column {
                            id: layout
                            width: parent.width

                            Text {
                                text: "deviceId: " + modelData.deviceId
                            }

                            Text {
                                text: "displayName: " + modelData.displayName
                            }

                            Text {
                                text: {
                                    switch(modelData.position) {
                                    case Camera.UnspecifiedPosition:
                                        return "position: UnspecifiedPosition"
                                    case Camera.BackFace:
                                        return "position: BackFace";
                                    case Camera.FrontFace:
                                        return "position: FrontFace"
                                    default:
                                        return "Unknown"
                                    }
                                }
                            }

                            Rectangle {
                                width: parent.width
                                height: units.gu(0.1)
                                color: "green"
                            }

                        }

                        MouseArea {
                            anchors.fill: parent
                            onClicked: {
                                camera.deviceId = modelData.deviceId
                                listview.currentIndex = index
                            }
                        }
                    }
                }

                VideoOutput {
                    id: output
                    source: camera
                    width: parent.width
                    height: parent.height - listview.height
                    focus : visible

                    MouseArea {
                        anchors.fill: parent
                        onClicked: {
                            console.log("going to capture to: " + path)
                            capture.captureToLocation(path);
                            output.visible = false
                            photoPreview.visible = true
                        }
                    }
                }

                Image {
                    id: photoPreview
                    width: parent.width
                    height: parent.height - listview.height
                    fillMode: Image.PreserveAspectFit

                    MouseArea {
                        anchors.fill: parent
                        onClicked: {
                            photoPreview.visible = false
                            output.visible = true
                        }
                    }

                    Component.onCompleted: {
                        visible = false
                    }
                }
            }
        }
    }
}

由于我们希望存储当前相机的所照的照片,我们必须得到能够存储照片的位置信息.我们在我们的Main.cpp中使用了如下的方法:

main.cpp

QString getPrivatePath()
{
    QString writablePath = QStandardPaths::
            writableLocation(QStandardPaths::DataLocation);
    qDebug() << "writablePath: " << writablePath;

    QString absolutePath = QDir(writablePath).absolutePath();
    qDebug() << "absoluePath: " << absolutePath;

    absolutePath += ".liu-xiao-guo/photos";

    // We need to make sure we have the path for storage
    QDir dir(absolutePath);
    if ( dir.mkpath(absolutePath) ) {
        qDebug() << "Successfully created the path!";
    } else {
        qDebug() << "Fails to create the path!";
    }

    qDebug() << "private path: " << absolutePath;

    return absolutePath;
}

    QString path = getPrivatePath();
    qDebug() << "final path: " << path;
   ...
    context->setContextProperty("path", QVariant::fromValue(path));

通过这样的方法,我们可以在我们的QML的代码中直接引用path来存储我们的照片.

运行我们的应用:

   


从上面的图中,我们可以看到所有相机的列表信息.我们可以随意选择我们所需要的相机,并进行拍摄.当然我们也可以进行录像的动作.具体的实现,可以参阅文章"在Ubuntu手机平台中使用Camera API来录像".


作者:UbuntuTouch 发表于2016/5/30 11:47:27 原文链接
阅读:318 评论:0 查看评论

Read more
UbuntuTouch

在Ubuntu的手机设计中,我们可以利用ContactModel API接口来读取我们的电话本信息.我们可以通过如下的API接口:

availableManagers 

来获取我们所有的电话本的manager.也就是有多少个电话本.通过设置:

manager 
来选择不同的电话本来读取信息.所有的电话本信息,我们可以在如下的API中读取:

contacts : list<Contact>
每个Contact包含一个电话本每个项的所有的信息:

address : Address
addresses : list<Address>
anniversary : Anniversary
avatar : Avatar
birthday : Birthday
contactDetails : list<ContactDetail>
contactId : int
displayLabel : DisplayLabel
email : EmailAddress
emails : list<EmailAddress>
extendedDetail : ExtendedDetail
extendedDetails : list<ExtendedDetail>
family : Family
favorite : Favorite
gender : Gender
geolocation : GeoLocation
globalPresence : GlobalPresence
guid : Guid
hobby : Hobby
manager : string
modified : bool
name : Name
nickname : Nickname
note : Note
onlineAccount : OnlineAccount
organization : Organization
organizations : list<Organization>
phoneNumber : PhoneNumber
phoneNumbers : list<PhoneNumber>
presence : Presence
ringtone : Ringtone
syncTarget : SyncTarget
tag : Tag
timestamp : Timestamp
type : enumeration
url : Url
urls : list<Url>
version : Version
我们可以根据我们所需要的信息来显示.

当我们显示信息的时候,我们可以通过SortOrder来排序我们的电话本中的项.

  ContactModel {
        id: mymodel
        sortOrders: [
            SortOrder {
                id: sortOrder

                detail: ContactDetail.Name
                field: Name.FirstName
                direction: Qt.DescendingOrder
            }
        ]
  ...
  }

我们也可以通过filter来过滤我们所需要的项,比如:

        filter: DetailFilter {
            id: favouritesFilter

            detail: ContactDetail.Favorite
            field: Favorite.Favorite
            value: "Yang"
            matchFlags: DetailFilter.MatchExactly
        }
这个filter将显示所有的Favorite的电话本,而如下的filter将显示lastName中有"Yang"的电话本:

        filter: DetailFilter {
            id: nameFilter

            detail: ContactDetail.Name
            field: Name.LastName
            value: "Yang"
            matchFlags: DetailFilter.MatchExactly
        }

我们的一个简单的例程如下:

Main.qml

import QtQuick 2.4
import Ubuntu.Components 1.3
import QtContacts 5.0
import Ubuntu.Components.ListItems 1.0 as ListItem

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "contactmodel.liu-xiao-guo"

    width: units.gu(100)
    height: units.gu(75)


    ContactModel {
        id: mymodel
        sortOrders: [
            SortOrder {
                id: sortOrder

                detail: ContactDetail.Name
                field: Name.FirstName
                direction: Qt.DescendingOrder
            }
        ]

        fetchHint: FetchHint {
            detailTypesHint: [ContactDetail.Avatar,
                ContactDetail.Name,
                ContactDetail.PhoneNumber]
        }

//        filter: DetailFilter {
//            id: favouritesFilter

//            detail: ContactDetail.Favorite
//            field: Favorite.Favorite
//            value: "Yang"
//            matchFlags: DetailFilter.MatchExactly
//        }

        filter: DetailFilter {
            id: nameFilter

            detail: ContactDetail.Name
            field: Name.LastName
            value: "Yang"
            matchFlags: DetailFilter.MatchExactly
        }
    }

    Component {
        id: highlight
        Rectangle {
            width: parent.width
            height: manager.delegate.height
            color: "lightsteelblue"; radius: 5
            Behavior on y {
                SpringAnimation {
                    spring: 3
                    damping: 0.2
                }
            }
        }
    }


    Page {
        header: PageHeader {
            id: pageHeader
            title: i18n.tr("contactmodel")
        }

        Item {
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                top:pageHeader.bottom
            }

            Column {
                anchors.fill: parent
                spacing: units.gu(0.5)

                Label {
                    text: "The contact managers:"
                    fontSize: "x-large"
                }

                ListView {
                    id: manager
                    clip: true
                    width: parent.width
                    height: units.gu(8)
                    highlight: highlight
                    model: mymodel.availableManagers
                    delegate: Item {
                        id: delegate
                        width: manager.width
                        height: man.height
                        Label {
                            id: man
                            text: modelData
                            fontSize: "large"
                        }

                        MouseArea {
                            anchors.fill: parent
                            onClicked: {
                                manager.currentIndex = index
                                // set the contact model manager
                                mymodel.manager = modelData
                            }
                        }
                    }
                }

                Rectangle {
                    id: divider
                    width: parent.width
                    height: units.gu(0.1)
                    color: "green"
                }

                CustomListItem {
                    id: storage
                    title.text: {
                        switch (mymodel.storageLocations ) {
                        case ContactModel.UserDataStorage:
                            return "UserDataStorage";
                        case ContactModel.SystemStorage:
                            return "SystemStorage";
                        default:
                            return "Unknown storage"
                        }
                    }
                }

                // Display the contact info here
                ListView {
                    width: parent.width
                    height: parent.height - manager.height - divider.height - storage.height
                    model: mymodel
                    delegate: ListItem.Subtitled {
                        text: contact.name.firstName + " " + contact.name.lastName
                        subText: contact.phoneNumber.number
                    }
                }
            }
        }

        Component.onCompleted: {
            console.log("count of manager: " + mymodel.availableManagers.length)
        }

    }
}

运行我们的代码:








作者:UbuntuTouch 发表于2016/5/31 11:24:06 原文链接
阅读:284 评论:0 查看评论

Read more
UbuntuTouch

在我们的Ubuntu SDK中,它提供了一个"Web App"的模版,它很方便地让我们把一个Web-based网页转换成一个Ubuntu的应用.大家可以参考我的文章"如何使用在线Webapp生成器生成安装包"来创建Web App.在今天的文章中,我们来介绍如实现一个全屏的Web App.


下面我们以百度地图为例.当我们利用我们的Ubuntu SDK创建一个应用时:

baidumap.desktop

[Desktop Entry]
Name=百度地图
Comment=webapp for baidumap
Type=Application
Icon=baidumap.png
Exec=webapp-container --enable-back-forward --store-session-cookies --webappUrlPatterns=https?://map.baidu.com/* http://map.baidu.com %u
Terminal=false
X-Ubuntu-Touch=true

细心的开发者可以看到在上面的"--enable-back-forward"选项.它让我们的Web App在最上面有一个title/header的地方,就像下面的图展示的那样:



在上面的图中,我们可以看到有一个"百度地图"的标题栏.它可以让我们返回以前的页面.

如果我们把上面的"--enable-back-forward"选项去掉,也就是:

Exec=webapp-container --store-session-cookies --webappUrlPatterns=https?://map.baidu.com/* http://map.baidu.com %u

那么我们的运行结果将是:



我们可以看出来,我们的header/title区域不见了.这样的好处是我们可以得到更多的显示区域.当然我们还是可以看到我们的indicator区域.

对于一些游戏的页面来说,能有一个全屏的用户界面是一个至高无上的需求,那么,我们可以更进一步来做如下的修改.我们添加一个"--fullscreen"的选项.

[Desktop Entry]
Name=百度地图
Comment=webapp for baidumap
Type=Application
Icon=baidumap.png
Exec=webapp-container --fullscreen -cookies --webappUrlPatterns=https?://map.baidu.com/* http://map.baidu.com %u
Terminal=false
X-Ubuntu-Touch=true

重新运行我们的项目:



我们可以看到一个全屏的应用,甚至连indicator的影子都没有.

更多关于Web App开发的信息可以在网站找到.



作者:UbuntuTouch 发表于2016/6/23 9:30:22 原文链接
阅读:290 评论:0 查看评论

Read more
UbuntuTouch

在Ubuntu SDK中,它提供了一个PlaceSearchModel接口.通过这个接口,我们可以在规定的区域里搜寻我们需要的地点,比如,KFC, Pizzar,或银行.在今天的教程中,我们来利用这个API来展示如何搜寻我们的兴趣点.

   


PlaceSearchModel的基本用法:

    PlaceSearchModel {
        id: searchModel
        plugin: myPlugin

        searchTerm: "KFC"
        searchArea: QtPositioning.circle(curLocation)

        Component.onCompleted: update()
    }


这里的searchTerm就是我们需要搜索的关键词.searchArea标示的是我们以什么为中心进行搜索.每当有一个新的关键词要进行搜索时,我们可以通过如下的方式来做:

                    searchModel.searchTerm = text
                    searchModel.update();

这里的plugin是已经存在于我们的系统的plugin.我们实际上是可以通过如下的方式来得到系统所有的plugin的:

    Plugin {
        id: plugin

        // Set the default one
        Component.onCompleted: {
            name = availableServiceProviders[0]
        }
    }

具体的使用方法可以参阅文章"在Ubuntu手机上利用Map API来显示地图并动态显示标记".

当一个地点被成功搜寻后,它返回的数据如下:


我们可以通过这些数据来显示我们得到位置的信息.

基于这些理解,我们做出了我们的例程:

Main.qml

import QtQuick 2.4
import Ubuntu.Components 1.3
import Ubuntu.Components.Popups 1.3
import QtLocation 5.3
import QtPositioning 5.2

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "placesearchmodel.liu-xiao-guo"

    width: units.gu(60)
    height: units.gu(85)

    property bool centerCurrent: true

    Plugin {
        id: myPlugin
        name: "osm"
    }

    PositionSource {
        id: positionSource
        property variant lastSearchPosition: curLocation
        active: true
        updateInterval: 5000
        onPositionChanged:  {
            var currentPosition = positionSource.position.coordinate

            if ( centerCurrent ) {
                map.center = currentPosition
            }

            var distance = currentPosition.distanceTo(lastSearchPosition)
            //            if (distance > 500) {
            // 500m from last performed pizza search
            lastSearchPosition = currentPosition
            searchModel.searchArea = QtPositioning.circle(currentPosition)
            searchModel.update()
            //            }
        }
    }

    property variant curLocation: QtPositioning.coordinate( 59.93, 10.76, 0)

    PlaceSearchModel {
        id: searchModel
        plugin: myPlugin

        searchTerm: "KFC"
        searchArea: QtPositioning.circle(curLocation)

        Component.onCompleted: update()
    }

    Connections {
        target: searchModel
        onStatusChanged: {
            if (searchModel.status == PlaceSearchModel.Error)
                console.log(searchModel.errorString());
        }
    }

    Component {
        id: pop
        Popover {
            id: popover
            width: units.gu(20)
            property var model
            Column {
                anchors {
                    left: parent.left
                    right: parent.right
                }

                Label {
                    anchors.horizontalCenter: parent.horizontalCenter
                    text: {
                        switch (model.type) {
                        case PlaceSearchModel.UnknownSearchResult:
                            return "type: UnknownSearchResult"
                        case PlaceSearchModel.PlaceResult:
                            return "type: PlaceResult"
                        case PlaceSearchModel.ProposedSearchResult:
                            return "type: ProposedSearchResult"
                        }
                    }
                    fontSize: "medium"
                }

                Label {
                    anchors.horizontalCenter: parent.horizontalCenter
                    text: "title: " + model.title
                    fontSize: "medium"
                }

                Label {
                    anchors.horizontalCenter: parent.horizontalCenter
                    text: "distance: " + (model.distance).toFixed(2) + "m"
                    fontSize: "medium"
                }

                Label {
                    anchors.horizontalCenter: parent.horizontalCenter
                    text: "sponsored: " + model.sponsored
                    fontSize: "medium"
                }
            }
        }
    }

    Page {
        id: page
        header: standardHeader

        PageHeader {
            id: standardHeader
            visible: page.header === standardHeader
            title: "PlaceSearchModel"
            trailingActionBar.actions: [
                Action {
                    iconName: "edit"
                    text: "Edit"
                    onTriggered: page.header = editHeader
                }
            ]
        }

        PageHeader {
            id: editHeader
            visible: page.header === editHeader
            leadingActionBar.actions: [
                Action {
                    iconName: "back"
                    text: "Back"
                    onTriggered: {
                        page.header = standardHeader
                    }
                }
            ]
            contents: TextField {
                id: input
                anchors {
                    left: parent.left
                    right: parent.right
                    verticalCenter: parent.verticalCenter
                }
                placeholderText: "input words .."
                text: "KFC"

                onAccepted: {
                    // clear all of the markers
                    mapItemView.model.reset()

                    searchModel.searchTerm = text
                    searchModel.update();
                }
            }
        }

        Item  {
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                top: page.header.bottom
            }

            Map {
                id: map
                anchors.fill: parent
                plugin: Plugin {
                    name: "osm"
                }
                center: curLocation
                zoomLevel: 13

                gesture {
                    onPanFinished: {
                        centerCurrent = false
                    }
                }

                MapCircle {
                    id: mylocation
                    center: positionSource.position.coordinate
                    radius: units.gu(4)
                    color: "yellow"
                }

                MapItemView {
                    id: mapItemView
                    model: searchModel
                    delegate: MapQuickItem {
                        id: mapitem
                        coordinate: place.location.coordinate

                        anchorPoint.x: image.width * 0.5
                        anchorPoint.y: image.height

                        sourceItem: Column {
                            Image {
                                width: units.gu(3)
                                height: width
                                id: image
                                source: "marker.png"
                            }
                            Text { text: title; font.bold: true }
                        }

                        MouseArea {
                            anchors.fill: parent
                            onClicked: {
                                onClicked: PopupUtils.open(pop, mapitem, { 'model': model })
                            }
                        }
                    }
                }

                Component.onCompleted: {
                    zoomLevel = 15
                }
            }
        }

    }
}


整个项目的源码在:https://github.com/liu-xiao-guo/PlaceSearchModel



            
作者:UbuntuTouch 发表于2016/6/21 8:40:59 原文链接
阅读:204 评论:0 查看评论

Read more
UbuntuTouch

Snap是一个在Ubuntu系统上一个新的技术.如果大家对这个技术还不是很理解的话,可以参阅文章"安装snap应用到Ubuntu 16.4桌面系统".我们知道MySQLTomcat是在Java服务器端常用的技术.那么我们怎么来把它打包到我们的snap应用中去呢?很多人可能以为这个很简单.我们可以直接在snapcraft.yaml中使用stage-package来安装现有的debian包,不就可以了吗?其实,由于在snap系统的安全性限制,在一个snap应用被成功安装后,它自己所在的目录是只读的,不可以写入任何的数据.我们只有规定的部分目录才是可以写入的.我们以hello例程为例,我们有如下的目录可以使用:


在上面,有两个最重要的目录:

  • SNAP_DATA
  • SNAP
在我们snap我们应用的时候,我们需要configure我们的应用尽量使用上面的两个目录,而不需要hard-code我们的目录.特别是由于我们的应用不能向自己的安装目录SNAP写入任何的数据(read-only),所以,我们必须把我们的数据库及需要写入数据的文件设置到SNAP_DATA目录中.另外必须注意的是,访问SNAP_DATA文件目录时需要root权限.这对于一个daemon的应用来说,应该没有问题,但是对于一般的应用来说,我们需要使用sudo来访问才可以访问到数据库.


1)snap Tomcat


在我们的snapcraft中有一个例程叫做"tomcat-webapp-demo".它提供了一个很好的基础.

snapcraft.yaml

name: tomcat-webapp-demo
version: 1.0
architectures:
 - amd64
summary: Demo of Tomcat-hosted Webapp
description: This is a demo snap of a Tomcat-hosted webapp produced by snapcraft with maven.
confinement: strict

apps:
 tomcat:
   command: bin/wrapper
   daemon: simple
   plugs: [network-bind]

parts:
    webapp:
        plugin: maven
        source: https://github.com/lool/snappy-mvn-demo.git
        source-type: git
    tomcat:
        plugin: dump
        source: https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.29/bin/apache-tomcat-8.0.29.tar.gz
    local-files:
        plugin: make
        source: .

从上面我们可以看出来,它使用了dump plugin.这个plugin只有在snapcraft 2.14中才开始适使用,并最终代替copy plugin. 对于以前的版本我们可以使用tar-content来代替dump.从上面的代码中可以看出来,它知己从官网上下载所需要的版本,并做一个安装.在启动的时候,它使用bin/wrapper来启动.wrapper的内容如下:

wrapper

#!/bin/sh

set -e
set -x

# installation pathes are based of CATALINA_HOME
export CATALINA_HOME="$SNAP"
# writable pathes are based of CATALINA_BASE
export CATALINA_BASE="$SNAP_DATA"

# create runtime data
mkdir -p "$CATALINA_BASE/logs"
mkdir -p "$CATALINA_BASE/temp"

if ! [ -d $CATALINA_BASE/conf ]; then
	echo "conf directory does not exist"
    cp -rd $CATALINA_HOME/tomcat-conf $CATALINA_BASE/conf
fi

if ! [ -d $CATALINA_BASE/webapps ]; then
	echo "webapps directory  does not exist"
    cp -rd $CATALINA_HOME/webapps $CATALINA_BASE/
    cp $CATALINA_HOME/war/*.war $CATALINA_BASE/webapps/
fi

$CATALINA_HOME/bin/catalina.sh run

从上面的代码中,我们可以看出来我们已经把我们所需要的目录用SNAP及SNAP_DATA来代替了.


2)snap MySQL


我们可以先看一下文章"Snapping Nextcloud: MySQL".从该文章中,我们可以看出来,直接通过stage-package的方式来snap MySQL是不可能的.我们可以直接编译MySQL源码的方式来snap MySQL.

snapcraft.yaml

    # Download the boost headers for MySQL. Note that the version used may need to
    # be updated if the version of MySQL changes.
    boost:
        plugin: copy
        source: http://sourceforge.net/projects/boost/files/boost/1.59.0/boost_1_59_0.tar.gz
        files:
          '*': boost/
        snap:
          - -*
        
    mysql:
        plugin: cmake
        source: https://github.com/kyrofa/mysql-server.git
        source-type: git
        source-branch: feature/support_no_setpriority
        after: [boost]
        configflags:
          - -DWITH_BOOST=$SNAPCRAFT_STAGE/boost
          - -DWITH_INNODB_PAGE_CLEANER_PRIORITY=OFF
          - -DCMAKE_INSTALL_PREFIX=/
          - -DBUILD_CONFIG=mysql_release
          - -DWITH_UNIT_TESTS=OFF
          - -DWITH_EMBEDDED_SERVER=OFF
          - -DWITH_EMBEDDED_SHARED_LIBRARY=OFF
          - -DWITH_ARCHIVE_STORAGE_ENGINE=OFF
          - -DWITH_BLACKHOLE_STORAGE_ENGINE=OFF
          - -DWITH_FEDERATED_STORAGE_ENGINE=OFF
          - -DWITH_PARTITION_STORAGE_ENGINE=OFF
          - -DINSTALL_MYSQLTESTDIR=
        build-packages:
          - wget
          - g++
          - cmake
          - bison
          - libncurses5-dev
          - libaio-dev
        stage:
         # Remove scripts that we'll be replacing with our own
          - -support-files/mysql.server
        snap:
         # Remove scripts that we'll be replacing with our own
         - -support-files/mysql.server

         # Remove unused binaries that waste space
         - -bin/innochecksum
         - -bin/lz4_decompress
         - -bin/myisam*
         - -bin/mysqladmin
         - -bin/mysqlbinlog
         - -bin/mysql_client_test
         - -bin/mysql_config*
         - -bin/mysqld_multi
         - -bin/mysqldump*
         - -bin/mysqlimport
         - -bin/mysql_install_db
         - -bin/mysql_plugin
         - -bin/mysqlpump
         - -bin/mysql_secure_installation
         - -bin/mysqlshow
         - -bin/mysqlslap
         - -bin/mysql_ssl_rsa_setup
         - -bin/mysqltest
         - -bin/mysql_tzinfo_to_sql
         - -bin/perror
         - -bin/replace
         - -bin/resolveip
         - -bin/resolve_stack_dump
         - -bin/zlib_decompress

    # Copy over our MySQL scripts
    mysql-customizations:
        plugin: copy
        files:
           # This one is what creates the initial database and account for ownCloud.
           src/mysql/start_mysql: bin/
           src/mysql/my.cnf: my.cnf
           src/mysql/mysql.server: support-files/
           sample.war: war/sample.war
           mysql-connector-java-6.0.3.jar: lib/mysql-connector-java-6.0.3.jar

由于编译MySQL时需要用到boost库,所以直接把boost库的源码下载下来,当然,在我们snap时,我们并不需要它,所以:

        snap:
          - -*

就想我们之前snap我们的tomcat时一样,我们需要对MySQL的配置进行改变才可以:


src/mysql/my.cnf


[mysqld]
user=root
max_allowed_packet=100M
secure-file-priv=NULL
port=3306
bind-address=0.0.0.0
[client]
protocol=tcp

在这里,我们配置了所需要的port号码及使用的协议TCP.这样我们的数据库可以位于任何一个单独的服务器中(我们可以修改这里的bind-address).在我们的设计中,MySQL的数据库位于本机.这样当我们的MySQL服务器运行时,可以看到:

liuxg@liuxg:~$ netstat -lnp46 | grep -w 3306
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      -               

src/mysql/mysql.server

这个文件是我们在我们的下面的start_mysql的脚本中被使用的:

$SNAP/support-files/mysql.server start

该脚本的内容如下:

#!/bin/sh
# Copyright Abandoned 1996 TCX DataKonsult AB & Monty Program KB & Detron HB
# This file is public domain and comes with NO WARRANTY of any kind

# MySQL daemon start/stop script.

# Usually this is put in /etc/init.d (at least on machines SYSV R4 based
# systems) and linked to /etc/rc3.d/S99mysql and /etc/rc0.d/K01mysql.
# When this is done the mysql server will be started when the machine is
# started and shut down when the systems goes down.

# Comments to support chkconfig on RedHat Linux
# chkconfig: 2345 64 36
# description: A very fast and reliable SQL database engine.

# Comments to support LSB init script conventions
### BEGIN INIT INFO
# Provides: mysql
# Required-Start: $local_fs $network $remote_fs
# Should-Start: ypbind nscd ldap ntpd xntpd
# Required-Stop: $local_fs $network $remote_fs
# Default-Start:  2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop MySQL
# Description: MySQL is a very fast and reliable SQL database engine.
### END INIT INFO
 
# If you install MySQL on some other places than /, then you
# have to do one of the following things for this script to work:
#
# - Run this script from within the MySQL installation directory
# - Create a /etc/my.cnf file with the following information:
#   [mysqld]
#   basedir=<path-to-mysql-installation-directory>
# - Add the above to any other configuration file (for example ~/.my.ini)
#   and copy my_print_defaults to /usr/bin
# - Add the path to the mysql-installation-directory to the basedir variable
#   below.
#
# If you want to affect other MySQL variables, you should make your changes
# in the /etc/my.cnf, ~/.my.cnf or other MySQL configuration files.

# If you change base dir, you must also change datadir. These may get
# overwritten by settings in the MySQL configuration files.

basedir=$SNAP
datadir=$SNAP_DATA/mysql

# Default value, in seconds, afterwhich the script should timeout waiting
# for server start. 
# Value here is overriden by value in my.cnf. 
# 0 means don't wait at all
# Negative numbers mean to wait indefinitely
service_startup_timeout=900

# Lock directory for RedHat / SuSE.
lockdir="$SNAP_DATA/mysql/lock"
lock_file_path="$lockdir/mysql"

# The following variables are only set for letting mysql.server find things.

# Set some defaults
mysqld_pid_file_path=
if test -z "$basedir"
then
  basedir=/
  bindir=//bin
  if test -z "$datadir"
  then
    datadir=//data
  fi
  sbindir=//bin
  libexecdir=//bin
else
  bindir="$basedir/bin"
  if test -z "$datadir"
  then
    datadir="$basedir/data"
  fi
  sbindir="$basedir/sbin"
  libexecdir="$basedir/libexec"
fi

# datadir_set is used to determine if datadir was set (and so should be
# *not* set inside of the --basedir= handler.)
datadir_set=

#
# Use LSB init script functions for printing messages, if possible
#
lsb_functions="/lib/lsb/init-functions"
if test -f $lsb_functions ; then
  . $lsb_functions
else
  log_success_msg()
  {
    echo " SUCCESS! $@"
  }
  log_failure_msg()
  {
    echo " ERROR! $@"
  }
fi

PATH="/sbin:/usr/sbin:/bin:/usr/bin:$basedir/bin"
export PATH

mode=$1    # start or stop

[ $# -ge 1 ] && shift


other_args="$*"   # uncommon, but needed when called from an RPM upgrade action
           # Expected: "--skip-networking --skip-grant-tables"
           # They are not checked here, intentionally, as it is the resposibility
           # of the "spec" file author to give correct arguments only.

case `echo "testing\c"`,`echo -n testing` in
    *c*,-n*) echo_n=   echo_c=     ;;
    *c*,*)   echo_n=-n echo_c=     ;;
    *)       echo_n=   echo_c='\c' ;;
esac

parse_server_arguments() {
  for arg do
    case "$arg" in
      --basedir=*)  basedir=`echo "$arg" | sed -e 's/^[^=]*=//'`
                    bindir="$basedir/bin"
		    if test -z "$datadir_set"; then
		      datadir="$basedir/data"
		    fi
		    sbindir="$basedir/sbin"
		    libexecdir="$basedir/libexec"
        ;;
      --datadir=*)  datadir=`echo "$arg" | sed -e 's/^[^=]*=//'`
		    datadir_set=1
	;;
      --pid-file=*) mysqld_pid_file_path=`echo "$arg" | sed -e 's/^[^=]*=//'` ;;
      --service-startup-timeout=*) service_startup_timeout=`echo "$arg" | sed -e 's/^[^=]*=//'` ;;
    esac
  done
}

wait_for_pid () {
  verb="$1"           # created | removed
  pid="$2"            # process ID of the program operating on the pid-file
  pid_file_path="$3" # path to the PID file.

  i=0
  avoid_race_condition="by checking again"

  while test $i -ne $service_startup_timeout ; do

    case "$verb" in
      'created')
        # wait for a PID-file to pop into existence.
        test -s "$pid_file_path" && i='' && break
        ;;
      'removed')
        # wait for this PID-file to disappear
        test ! -s "$pid_file_path" && i='' && break
        ;;
      *)
        echo "wait_for_pid () usage: wait_for_pid created|removed pid pid_file_path"
        exit 1
        ;;
    esac

    # if server isn't running, then pid-file will never be updated
    if test -n "$pid"; then
      if kill -0 "$pid" 2>/dev/null; then
        :  # the server still runs
      else
        # The server may have exited between the last pid-file check and now.  
        if test -n "$avoid_race_condition"; then
          avoid_race_condition=""
          continue  # Check again.
        fi

        # there's nothing that will affect the file.
        log_failure_msg "The server quit without updating PID file ($pid_file_path)."
        return 1  # not waiting any more.
      fi
    fi

    echo $echo_n ".$echo_c"
    i=`expr $i + 1`
    sleep 1

  done

  if test -z "$i" ; then
    log_success_msg
    return 0
  else
    log_failure_msg
    return 1
  fi
}

# Get arguments from the my.cnf file,
# the only group, which is read from now on is [mysqld]
if test -x ./bin/my_print_defaults
then
  print_defaults="./bin/my_print_defaults"
elif test -x $bindir/my_print_defaults
then
  print_defaults="$bindir/my_print_defaults"
elif test -x $bindir/mysql_print_defaults
then
  print_defaults="$bindir/mysql_print_defaults"
else
  # Try to find basedir in /etc/my.cnf
  conf=/etc/my.cnf
  print_defaults=
  if test -r $conf
  then
    subpat='^[^=]*basedir[^=]*=\(.*\)$'
    dirs=`sed -e "/$subpat/!d" -e 's//\1/' $conf`
    for d in $dirs
    do
      d=`echo $d | sed -e 's/[ 	]//g'`
      if test -x "$d/bin/my_print_defaults"
      then
        print_defaults="$d/bin/my_print_defaults"
        break
      fi
      if test -x "$d/bin/mysql_print_defaults"
      then
        print_defaults="$d/bin/mysql_print_defaults"
        break
      fi
    done
  fi

  # Hope it's in the PATH ... but I doubt it
  test -z "$print_defaults" && print_defaults="my_print_defaults"
fi

#
# Read defaults file from 'basedir'.   If there is no defaults file there
# check if it's in the old (depricated) place (datadir) and read it from there
#

extra_args=""
if test -r "$basedir/my.cnf"
then
  extra_args="-e $basedir/my.cnf"
else
  if test -r "$datadir/my.cnf"
  then
    extra_args="-e $datadir/my.cnf"
  fi
fi

parse_server_arguments `$print_defaults $extra_args mysqld server mysql_server mysql.server`

#
# Set pid file if not given
#
if test -z "$mysqld_pid_file_path"
then
  mysqld_pid_file_path=$datadir/`hostname`.pid
else
  case "$mysqld_pid_file_path" in
    /* ) ;;
    * )  mysqld_pid_file_path="$datadir/$mysqld_pid_file_path" ;;
  esac
fi

case "$mode" in
  'start')
    # Start daemon

    # Safeguard (relative paths, core dumps..)
    cd $basedir

    echo $echo_n "Starting MySQL"
    if test -x $bindir/mysqld_safe
    then
      # Give extra arguments to mysqld with the my.cnf file. This script
      # may be overwritten at next upgrade.
      $bindir/mysqld_safe --datadir="$datadir" --pid-file="$mysqld_pid_file_path" --lc-messages-dir="$SNAP/share" $other_args >/dev/null 2>&1 &
      wait_for_pid created "$!" "$mysqld_pid_file_path"; return_value=$?

      # Make lock for RedHat / SuSE
      if test -w "$lockdir"
      then
        touch "$lock_file_path"
      fi

      exit $return_value
    else
      log_failure_msg "Couldn't find MySQL server ($bindir/mysqld_safe)"
    fi
    ;;

  'stop')
    # Stop daemon. We use a signal here to avoid having to know the
    # root password.

    if test -s "$mysqld_pid_file_path"
    then
      # signal mysqld_safe that it needs to stop
      touch "$mysqld_pid_file_path.shutdown"

      mysqld_pid=`cat "$mysqld_pid_file_path"`

      if (kill -0 $mysqld_pid 2>/dev/null)
      then
        echo $echo_n "Shutting down MySQL"
        kill $mysqld_pid
        # mysqld should remove the pid file when it exits, so wait for it.
        wait_for_pid removed "$mysqld_pid" "$mysqld_pid_file_path"; return_value=$?
      else
        log_failure_msg "MySQL server process #$mysqld_pid is not running!"
        rm "$mysqld_pid_file_path"
      fi

      # Delete lock for RedHat / SuSE
      if test -f "$lock_file_path"
      then
        rm -f "$lock_file_path"
      fi
      exit $return_value
    else
      log_failure_msg "MySQL server PID file could not be found!"
    fi
    ;;

  'restart')
    # Stop the service and regardless of whether it was
    # running or not, start it again.
    if $0 stop  $other_args; then
      $0 start $other_args
    else
      log_failure_msg "Failed to stop running server, so refusing to try to start."
      exit 1
    fi
    ;;

  'reload'|'force-reload')
    if test -s "$mysqld_pid_file_path" ; then
      read mysqld_pid <  "$mysqld_pid_file_path"
      kill -HUP $mysqld_pid && log_success_msg "Reloading service MySQL"
      touch "$mysqld_pid_file_path"
    else
      log_failure_msg "MySQL PID file could not be found!"
      exit 1
    fi
    ;;
  'status')
    # First, check to see if pid file exists
    if test -s "$mysqld_pid_file_path" ; then 
      read mysqld_pid < "$mysqld_pid_file_path"
      if kill -0 $mysqld_pid 2>/dev/null ; then 
        log_success_msg "MySQL running ($mysqld_pid)"
        exit 0
      else
        log_failure_msg "MySQL is not running, but PID file exists"
        exit 1
      fi
    else
      # Try to find appropriate mysqld process
      mysqld_pid=`pidof $libexecdir/mysqld`

      # test if multiple pids exist
      pid_count=`echo $mysqld_pid | wc -w`
      if test $pid_count -gt 1 ; then
        log_failure_msg "Multiple MySQL running but PID file could not be found ($mysqld_pid)"
        exit 5
      elif test -z $mysqld_pid ; then 
        if test -f "$lock_file_path" ; then 
          log_failure_msg "MySQL is not running, but lock file ($lock_file_path) exists"
          exit 2
        fi 
        log_failure_msg "MySQL is not running"
        exit 3
      else
        log_failure_msg "MySQL is running but PID file could not be found"
        exit 4
      fi
    fi
    ;;
    *)
      # usage
      basename=`basename "$0"`
      echo "Usage: $basename  {start|stop|restart|reload|force-reload|status}  [ MySQL server options ]"
      exit 1
    ;;
esac

exit 0

这个脚本的内容其实和标准的从MySQL编译后的脚本没有什么大的差别.除了如下的地方发生改变:

basedir=$SNAP
datadir=$SNAP_DATA/mysql

这个地方的改变是为了提供我们运行文件的参考目录位置及数据库的文件位置.

src/mysql/start_mysql



这是一个我们需要的脚本来真正启动我们的MySQL服务器:

 mysql:
   command: start_mysql
   stop-command: support-files/mysql.server stop
   daemon: simple
   plugs: [network, network-bind]

显然这是一个daemon应用.它的内容如下:

#!/bin/sh

root_option_file="$SNAP_DATA/mysql/root.ini"
demouser_password_file="$SNAP_DATA/mysql/demouser_password"
mysqld_pid_file_path=$SNAP_DATA/mysql/`hostname`.pid
#mysql_socket_file_path=$SNAP_DATA/mysql/mysql.sock
new_install=false

# Make sure the database is initialized (this is safe to run if already
# initialized)
mysqld --initialize-insecure --basedir="$SNAP" --datadir="$SNAP_DATA/mysql" --lc-messages-dir="$SNAP/share"

# If the above command succeeded, it means this is a new installation.
if [ $? -eq 0 ]; then
	echo "it is new install"
	new_install=true
fi

# Start mysql
$SNAP/support-files/mysql.server start

# Initialize new installation if necessary.
if [ $new_install = true ]; then
	# Generate a password for the root mysql user.
	echo -n "Generating root mysql password... "
	root_password="123"
	echo "done."

	# Generate a password for the owncloud mysql user.
	echo -n "Generating owncloud mysql password... "
	demouser_password="123"
	echo "done."

	# Save root user information
	echo "writing client"
	echo "[client]" >> $root_option_file
	echo "writing user=root"
	echo "user=root" >> $root_option_file
	chmod 600 $root_option_file
	
	echo "dump the root option file"
	cat $root_option_file

	# Now set everything up in one step:
	# 1) Set the root user's password
	# 2) Create the 'demouser' user
	# 3) Create the 'demodb' database
	# 4) Grant the 'demodb' user privileges on the 'demodb' database
	echo -n "Setting up users and owncloud database... "
	mysql --defaults-file=$root_option_file <<SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY '$root_password';
CREATE USER 'demouser'@'localhost' IDENTIFIED BY '$demouser_password';
CREATE DATABASE demodb;
GRANT ALL PRIVILEGES ON demodb.* TO 'demouser'@'localhost' IDENTIFIED BY '$demouser_password';
USE demodb;
create table Employees
    (
     id int not null,
     age int not null,
     first varchar (255),
     last varchar (255)
    );
INSERT INTO Employees VALUES (100, 18, 'Zara', 'Ali');
INSERT INTO Employees VALUES (101, 25, 'Mahnaz', 'Fatma');    
INSERT INTO Employees VALUES (102, 30, 'Zaid', 'Khan');
INSERT INTO Employees VALUES (103, 28, 'Sumit', 'Mittal');
SQL
	if [ $? -eq 0 ]; then
		echo "done."
	else
		echo "Failed to initialize-- reverting..."
		$SNAP/support-files/mysql.server stop
		rm -rf $SNAP_DATA/mysql/*
	fi

	# Now the root mysql user has a password. Save that as well.
	echo "writing root password"
	echo "password=$root_password" >> $root_option_file
fi

# Wait here until mysql is running
echo "Waiting for server..."
#while [ ! -f "$mysqld_pid_file_path" -o ! -S "$mysql_socket_file_path" ]; do
while [ ! -f "$mysqld_pid_file_path" -o ]; do
	sleep 1
done

# Check and upgrade mysql tables if necessary. This will return 0 if the upgrade
# succeeded, in which case we need to restart mysql.
echo "Checking/upgrading mysql tables if necessary..."
mysql_upgrade --defaults-file=$root_option_file
if [ $? -eq 0 ]; then
	echo "Restarting mysql server after upgrade..."
	$SNAP/support-files/mysql.server restart

	echo "Waiting for server to come back after upgrade..."
#	while [ ! -f "$mysqld_pid_file_path" -o ! -S "$mysql_socket_file_path" ]; do
	while [ ! -f "$mysqld_pid_file_path" -o ]; do
        	sleep 1
	done
fi

# If this was a new installation, wait until the server is all up and running
# before saving off the owncloud user's password. This way the presence of the
# file can be used as a signal that mysql is ready to be used.
if [ $new_install = true ]; then
	echo "$demouser_password" > $demouser_password_file
fi

# Wait here until mysql exits (turn a forking service into simple). This is
# only needed for Ubuntu Core 15.04, as 16.04 supports forking services.
mysqld_pid=$(cat "$mysqld_pid_file_path")
while kill -0 $mysqld_pid 2>/dev/null; do
	sleep 1
done

我们在table中也创建了几个记录.在这里也请大家注意,我hard-code了数据库的密码"123".这个密码在我们以后的数据库访问中需要用到.注意我们在这里已经创建了两个用户,一个叫做root,另外一个叫做demouser

	mysql --defaults-file=$root_option_file <<SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY '$root_password';
CREATE USER 'demouser'@'localhost' IDENTIFIED BY '$demouser_password';
CREATE DATABASE demodb;
GRANT ALL PRIVILEGES ON demodb.* TO 'demouser'@'localhost' IDENTIFIED BY '$demouser_password';
USE demodb;
create table Employees
    (
     id int not null,
     age int not null,
     first varchar (255),
     last varchar (255)
    );
INSERT INTO Employees VALUES (100, 18, 'Zara', 'Ali');
INSERT INTO Employees VALUES (101, 25, 'Mahnaz', 'Fatma');    
INSERT INTO Employees VALUES (102, 30, 'Zaid', 'Khan');
INSERT INTO Employees VALUES (103, 28, 'Sumit', 'Mittal');
SQL

如何调用MySQL命令行


我们在snapcraft.yaml文档中,定义了如下的command:

 mysql-client:
   command: mysql --defaults-file=$SNAP_DATA/mysql/root.ini --protocol=TCP
   plugs: [network, network-bind]

由于在MySQL的命令行中需要访问到数据库,需要root权限.我们可以通过su命令进入到root用户:



我们可以通过运行我们定义的命令来启动MySQL客户端.通过这个命令,我们可以创建我们的数据等操作.



3)JSP 数据库访问


最后,我们需要一个JSP的程序来访问我们的数据库.我们的设计如下:

index.jsp

<%@ page import="java.sql.*"%>
<html>
<head>
<title>JDBC Connection example</title>
</head>

<body>
<h1>JDBC Connection example</h1>

<%
  String user = "demouser";
  String password = "123";
  
  try {
    java.sql.Connection con;
    Class.forName("com.mysql.jdbc.Driver");
    con = DriverManager.getConnection("jdbc:mysql://localhost:3306/demodb", user, password);
    out.println (user + " account opens database successfully opened.");
    
    String query="select * from Employees";
    Statement stmt=con.createStatement();
	ResultSet rs=stmt.executeQuery(query);
%>	   
    <br />
    <h3>The data read from database is:</h3>
    <br />
    
	<table border="2">
	<tr>
		<td>Id</td>
		<td>Age</td>
		<td>First</td>
		<td>Last</td>
	</tr>
<%
	while(rs.next())
	{
%>
	<tr>
		<td><%=rs.getInt("id")%></td>
		<td><%=rs.getInt("age")%></td>
		<td><%=rs.getString("first")%></td>
		<td><%=rs.getString("last")%></td>
	</tr>
<%
	}
%>
	</table>
	
<%
    // close the connection
    rs.close();
    stmt.close();
    con.close();
  }
  catch(SQLException e) {
    out.println("SQLException caught: " +e.getMessage());
  }
%>

</body>
</html>


最终打包我们的应用,并部署我们的应用.我们可以看到最终的结果为:




在实际的使用中,我们也可以把我们已经开发好的war包放入到我们的tomcat的目录中:

    mysql-customizations:
        plugin: copy
        files:
           # This one is what creates the initial database and account for ownCloud.
           src/mysql/start_mysql: bin/
           src/mysql/my.cnf: my.cnf
           src/mysql/mysql.server: support-files/
           sample.war: war/sample.war
           mysql-connector-java-6.0.3.jar: lib/mysql-connector-java-6.0.3.jar

比如上面的sample.war,我们可以通过如下的方式来运行该应用:




更多关于snap的介绍,可以参阅文章"安装snap应用到Ubuntu 16.4桌面系统

作者:UbuntuTouch 发表于2016/8/16 10:57:12 原文链接
阅读:379 评论:0 查看评论

Read more
UbuntuTouch

在今天的例程中,我们将重点介绍如何使用Ubuntu手机SDK所提供的SDK来创建一个可以聊天的应用.通过这个例程,我们来展示如何利用Bluetooth API接口在两个Ubuntu手机或Ubuntu电脑上进行聊天.


    


1)创建一个Bluetooth Chat server


chatserver.cpp

#include "chatserver.h"

#include <qbluetoothserver.h>
#include <qbluetoothsocket.h>
#include <qbluetoothlocaldevice.h>

static const QLatin1String serviceUuid("e8e10f95-1a70-4b27-9ccf-02010264e9c8");

ChatServer::ChatServer(QObject *parent)
:   QObject(parent), rfcommServer(0)
{
}

ChatServer::~ChatServer()
{
    stopServer();
}

void ChatServer::startServer(const QBluetoothAddress& localAdapter)
{
    if (rfcommServer)
        return;

    rfcommServer = new QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
    connect(rfcommServer, SIGNAL(newConnection()), this, SLOT(clientConnected()));
    bool result = rfcommServer->listen(localAdapter);
    if (!result) {
        qWarning() << "Cannot bind chat server to" << localAdapter.toString();
        return;
    }

    //serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceRecordHandle, (uint)0x00010010);

    QBluetoothServiceInfo::Sequence classId;

    classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort));
    serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList,
                             classId);

    classId.prepend(QVariant::fromValue(QBluetoothUuid(serviceUuid)));

    serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);
    serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList,classId);

    serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceName, tr("Bt Chat Server"));
    serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceDescription,
                             tr("Example bluetooth chat server"));
    serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceProvider, tr("qt-project.org"));

    serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid));

    QBluetoothServiceInfo::Sequence publicBrowse;
    publicBrowse << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::PublicBrowseGroup));
    serviceInfo.setAttribute(QBluetoothServiceInfo::BrowseGroupList,
                             publicBrowse);

    QBluetoothServiceInfo::Sequence protocolDescriptorList;
    QBluetoothServiceInfo::Sequence protocol;
    protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap));
    protocolDescriptorList.append(QVariant::fromValue(protocol));
    protocol.clear();
    protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Rfcomm))
             << QVariant::fromValue(quint8(rfcommServer->serverPort()));
    protocolDescriptorList.append(QVariant::fromValue(protocol));
    serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList,
                             protocolDescriptorList);

    serviceInfo.registerService(localAdapter);
}

void ChatServer::stopServer()
{
    // Unregister service
    serviceInfo.unregisterService();

    // Close sockets
    qDeleteAll(clientSockets);

    // Close server
    delete rfcommServer;
    rfcommServer = 0;
}

void ChatServer::disconnect()
{
    qDebug() << "Going to disconnect in server";

    foreach (QBluetoothSocket *socket, clientSockets) {
        qDebug() << "sending data in server!";
        socket->close();
    }
}

void ChatServer::sendMessage(const QString &message)
{
    qDebug() << "Going to send message in server: " << message;
    QByteArray text = message.toUtf8() + '\n';

    foreach (QBluetoothSocket *socket, clientSockets) {
        qDebug() << "sending data in server!";
        socket->write(text);
    }
    qDebug() << "server sending done!";
}

void ChatServer::clientConnected()
{
    qDebug() << "clientConnected";

    QBluetoothSocket *socket = rfcommServer->nextPendingConnection();
    if (!socket)
        return;

    connect(socket, SIGNAL(readyRead()), this, SLOT(readSocket()));
    connect(socket, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
    clientSockets.append(socket);
    emit clientConnected(socket->peerName());
}

void ChatServer::clientDisconnected()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;

    emit clientDisconnected(socket->peerName());

    clientSockets.removeOne(socket);

    socket->deleteLater();
}

void ChatServer::readSocket()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;

    while (socket->canReadLine()) {
        QByteArray line = socket->readLine().trimmed();
        emit messageReceived(socket->peerName(),
                             QString::fromUtf8(line.constData(), line.length()));
    }
}


在这里我们通过QBluetoothServer来创建一个Bluetooth基于RFCOMM协议的server.我们发布一个基于这个协议的server.其它想连接这个服务器的client,必须寻找我们在这里所定义的ServiceUuid:

 classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort));
    serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList,
                             classId);

    classId.prepend(QVariant::fromValue(QBluetoothUuid(serviceUuid)));

这里所定义的serviceUuid:

static const QLatin1String serviceUuid("e8e10f95-1a70-4b27-9ccf-02010264e9c8");

2)创建一个Bluetooth client


我们可以创建一个Bluetooth的client用来发起一个向Bluetooth server的连接请求.

chatclient.cpp


#include "chatclient.h"

#include <qbluetoothsocket.h>

ChatClient::ChatClient(QObject *parent)
:   QObject(parent), socket(0)
{
}

ChatClient::~ChatClient()
{
    stopClient();
}

void ChatClient::startClient(const QBluetoothServiceInfo &remoteService)
{
    if (socket)
        return;

    // Connect to service
    socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
    qDebug() << "Create socket";
    socket->connectToService(remoteService);
    qDebug() << "ConnectToService done";

    connect(socket, SIGNAL(readyRead()), this, SLOT(readSocket()));
    connect(socket, SIGNAL(connected()), this, SLOT(connected()));
    connect(socket, SIGNAL(disconnected()), this, SIGNAL(disconnected()));
}

void ChatClient::stopClient()
{
    delete socket;
    socket = 0;
}

void ChatClient::readSocket()
{
    if (!socket)
        return;

    while (socket->canReadLine()) {
        QByteArray line = socket->readLine();
        emit messageReceived(socket->peerName(),
                             QString::fromUtf8(line.constData(), line.length()));
    }
}

void ChatClient::sendMessage(const QString &message)
{
    qDebug() << "Sending data in client: " + message;

    QByteArray text = message.toUtf8() + '\n';
    socket->write(text);
}

void ChatClient::connected()
{
    emit connected(socket->peerName());
}

void ChatClient::disconnect() {
    qDebug() << "Going to disconnect in client";
    if ( socket ) {
        qDebug() << "diconnecting...";
        socket->close();
    }
}

当我们发生一个请求的时候,我们可以通过如下的方式来连接:

chat.cpp

void Chat::connectToDevice(QString name)
{
    qDebug() << "Connecting to " << name;

    // Trying to get the service
    QBluetoothServiceInfo service;
    QMapIterator<QString, QBluetoothServiceInfo> i(remoteSelector->m_discoveredServices);
    bool found = false;
    while (i.hasNext()){
        i.next();
        QString key = i.key();
        if ( key == name ) {
            qDebug() << "The device is found";
            service = i.value();
            qDebug() << "value: " << i.value().device().address();
            found = true;
            break;
        }
    }

    if ( found ) {
        qDebug() << "Going to create client";
        ChatClient *client = new ChatClient(this);
        qDebug() << "Connecting...";

        connect(client, SIGNAL(messageReceived(QString,QString)),
                this, SIGNAL(showMessage(QString,QString)));
        connect(client, SIGNAL(disconnected()), this, SIGNAL(clientDisconnected()));
        connect(client, SIGNAL(disconnected()), this, SLOT(clientIsDisconnected()));
        connect(client, SIGNAL(connected(QString)), this, SIGNAL(connected(QString)));
        connect(this, SIGNAL(sendMessage(QString)), client, SLOT(sendMessage(QString)));
        connect(this, SIGNAL(disconnect()), client, SLOT(disconnect()));

        qDebug() << "Start client";
        client->startClient(service);

        clients.append(client);
    }
}



3)扫描Bluetooth device


我们可以通过如下的方法来扫描附件的Bluetooth设备:

remoteselector.cpp


#include <qbluetoothdeviceinfo.h>
#include <qbluetoothaddress.h>
#include <qbluetoothlocaldevice.h>

#include "remoteselector.h"

QT_USE_NAMESPACE

RemoteSelector::RemoteSelector(QBluetoothAddress &localAdapter, QObject *parent)
: QObject(parent)
{
    m_discoveryAgent = new QBluetoothServiceDiscoveryAgent(localAdapter);

    connect(m_discoveryAgent, SIGNAL(serviceDiscovered(QBluetoothServiceInfo)),
            this, SLOT(serviceDiscovered(QBluetoothServiceInfo)));
    connect(m_discoveryAgent, SIGNAL(finished()), this, SLOT(discoveryFinished()));
    connect(m_discoveryAgent, SIGNAL(canceled()), this, SLOT(discoveryFinished()));
}

RemoteSelector::~RemoteSelector()
{
    delete m_discoveryAgent;
}

void RemoteSelector::startDiscovery(const QBluetoothUuid &uuid)
{
    qDebug() << "startDiscovery";
    if (m_discoveryAgent->isActive()) {
        qDebug() << "stop the searching first";
        m_discoveryAgent->stop();
    }

    m_discoveryAgent->setUuidFilter(uuid);
    m_discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);

}

void RemoteSelector::stopDiscovery()
{
    qDebug() << "stopDiscovery";
    if (m_discoveryAgent){
        m_discoveryAgent->stop();
    }
}

QBluetoothServiceInfo RemoteSelector::service() const
{
    return m_service;
}

void RemoteSelector::serviceDiscovered(const QBluetoothServiceInfo &serviceInfo)
{
#if 0
    qDebug() << "Discovered service on"
             << serviceInfo.device().name() << serviceInfo.device().address().toString();
    qDebug() << "\tService name:" << serviceInfo.serviceName();
    qDebug() << "\tDescription:"
             << serviceInfo.attribute(QBluetoothServiceInfo::ServiceDescription).toString();
    qDebug() << "\tProvider:"
             << serviceInfo.attribute(QBluetoothServiceInfo::ServiceProvider).toString();
    qDebug() << "\tL2CAP protocol service multiplexer:"
             << serviceInfo.protocolServiceMultiplexer();
    qDebug() << "\tRFCOMM server channel:" << serviceInfo.serverChannel();
#endif

    QString remoteName;
    if (serviceInfo.device().name().isEmpty())
        remoteName = serviceInfo.device().address().toString();
    else
        remoteName = serviceInfo.device().name();

    qDebug() << "adding to the list....";
    qDebug() << "remoteName: "  << remoteName;
    m_discoveredServices.insert(remoteName, serviceInfo);
    emit newServiceFound();
}

void RemoteSelector::discoveryFinished()
{
    qDebug() << "discoveryFinished";
    emit finished();
}

我们可以通过调用startDiscovery()来扫描附近的设备.在调用时,我们可以设置我们想要的serviceUuid:

chat.cpp


void Chat::searchForDevices()
{      
    qDebug() << "search for devices!";
    if ( remoteSelector ) {
        delete remoteSelector;
        remoteSelector = NULL;
    }

    QBluetoothAddress adapter = QBluetoothAddress();
    remoteSelector = new RemoteSelector(adapter, this);

    connect(remoteSelector, SIGNAL(newServiceFound()), this, SLOT(newServiceFound()));

    remoteSelector->m_discoveredServices.clear();
    remoteSelector->startDiscovery(QBluetoothUuid(serviceUuid));
    connect(remoteSelector, SIGNAL(finished()), this, SIGNAL(discoveryFinished()));
}

这样就可以扫描到具有我们所需要的serviceUuid的设备来提供连接.在扫描时,Bluetooth server必须是在运行的状态.


4)应用UI设计


我们的应用UI设计比较简单.就想上面显示的图一样,我们最上面的显示当前正在运行该应用的Bluetooth server的设备.当我们点击该设备时,就开始向该设备发送连接请求.在我们的下面的对话框中,会显示连接的状态及对话:

Main.qml


import QtQuick 2.4
import Ubuntu.Components 1.3
import QtBluetooth 5.3

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "btchat.xiaoguo"

    width: units.gu(60)
    height: units.gu(85)

    function getBluetoothState(state) {
        switch (state ) {
        case BluetoothSocket.Unconnected:
            return "Unconnected";
        case BluetoothSocket.ServiceLookup:
            return "ServiceLookup";
        case BluetoothSocket.Connecting:
            return "Connecting";
        case BluetoothSocket.Connected:
            return "Connected";
        case BluetoothSocket.Bound:
            return "Bound";
        case BluetoothSocket.Closing:
            return "Closing";
        case BluetoothSocket.Listening:
            return "Listening";
        case BluetoothSocket.ServiceLookup:
            return "ServiceLookup";
        case BluetoothSocket.NoServiceSet:
            return "NoServiceSet";
        default:
            return "Unknow state"
        }
    }

    function appendMessage(msg, alignright) {
        mymodel.append({ "msg": msg, "alignright": alignright} )
        listview.positionViewAtIndex(mymodel.count - 1, ListView.Beginning)
    }

    Connections {
        target: chat
        onConnected: {
            console.log("Connected: " + name)
            appendMessage( "connected to " + name, false )
        }

        onDisconnected: {
            console.log("Disconnected: " + name )
            appendMessage( "disconnected " + name, false )
        }

        onClientDisconnected: {
            console.log("Client Disconnected")
            appendMessage("Client Disconnected", false)
        }

        onShowMessage: {
            console.log("sender: " + sender )
            console.log("message: " + message )
            message = message.replace(/(\r\n|\n|\r)/gm,"");
            var msg = '<font color = "green">' + message + '</font>';
            console.log("msg: " + msg)
            appendMessage(msg, false)
        }

        onNewServicesFound: {
            console.log("new services found!")
            devlist.model = list;
        }

        onDiscoveryFinished: {
            console.log("discovery finished");
            indicator.running = false;
        }
    }

    ListModel {
        id: mymodel
    }

    ListModel {
        id: services
    }

    BluetoothDiscoveryModel {
        id: btModel
        running: false
        discoveryMode: BluetoothDiscoveryModel.FullServiceDiscovery
        onRunningChanged : {
        }

        onErrorChanged: {
        }

        onServiceDiscovered: {
            console.log("service has been found!")
            services.append( {"service": service })
        }
        uuidFilter: "e8e10f95-1a70-4b27-9ccf-02010264e9c8"
    }

    BluetoothSocket {
        id: socket
        connected: true

        onSocketStateChanged: {
            console.log("socketState: " + socketState);
            console.log("Connected to server! ")
            appendMessage( "State: " + getBluetoothState(socketState) )
        }

        onStringDataChanged: {
            console.log("Received data: " )
            var data = "Going to send: " + socket.stringData;
            data = data.substring(0, data.indexOf('\n'))
            console.log(data);
            appendMessage("Received: " + data)
        }
    }

    Component {
        id: highlight
        Rectangle {
            width: devlist.width
            height: devlist.delegate.height
            color: "lightsteelblue"; radius: 5
            Behavior on y {
                SpringAnimation {
                    spring: 3
                    damping: 0.2
                }
            }
        }
    }

    Page {
        id: page
        header: standardHeader

        PageHeader {
            id: standardHeader
            visible: page.header === standardHeader
            title: "Bluetooth chat"
            trailingActionBar.actions: [
                Action {
                    iconName: "edit"
                    text: "Edit"
                    onTriggered: page.header = editHeader
                }
            ]
        }

        PageHeader {
            id: editHeader
            visible: page.header === editHeader
            leadingActionBar.actions: [
                Action {
                    iconName: "back"
                    text: "Back"
                    onTriggered: {
                        page.header = standardHeader
                    }
                }
            ]
            contents: Row {
                id: layout
                anchors {
                    left: parent.left
                    right: parent.right
                    verticalCenter: parent.verticalCenter
                }
                spacing: units.gu(2)

                TextField {
                    id: input
                    width: parent.width*2/3
                    placeholderText: "input words .."
                    text: "I love you!"

                    onAccepted: {
                        console.log("going to send: " + text)
                    }
                }

                Button {
                    text: "Send"
                    width: parent.width - input.width - layout.spacing;
                    onClicked: {
                        console.log("send is clicked")
                        console.log("chat length: " + input.text.length)
                        chat.sendMessage(input.text);
                        appendMessage(input.text, true)
                    }
                }
            }
        }

        Item {
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                top: page.header.bottom
            }

            ActivityIndicator {
                id: indicator
                anchors.centerIn: parent
            }

            Column {
                anchors.fill: parent

                Label {
                    text: "Devices are:"
                    fontSize: "x-large"
                }

                ListView {
                    id: devlist
                    width: parent.width
                    height: parent.height/4
                    model: services
                    highlight: highlight
                    delegate: Label {
                        width: parent.width
                        text: modelData
                        fontSize: "x-large"
                        MouseArea {
                            anchors.fill: parent
                            onClicked: {
                                console.log("it is selected")
                                chat.connectToDevice(modelData)
                            }
                        }
                    }
                }

                Rectangle {
                    width: parent.width
                    height: units.gu(0.4)
                    color: "green"
                }

                ListView {
                    id: listview
                    width: parent.width
                    height: parent.height - devlist.height
                    model: mymodel
                    delegate: Item {
                        width: page.width
                        height: txt.height * 1.2 /*+ div.height*/

                        Rectangle {
                            width: txt.contentWidth
                            height: parent.height
                            color: alignright? "green" : "white"
                            radius: units.gu(0.5)
                            anchors.right: alignright ? parent.right : undefined
                            anchors.verticalCenter: parent.verticalCenter
                        }

                        Label {
                            id: txt
                            width: parent.width*0.7
                            text: msg
                            anchors.right: alignright ? parent.right : undefined
                            horizontalAlignment: alignright ? Text.AlignRight :
                                                              Text.AlignLeft
                            fontSize: "large"
                            anchors.verticalCenter: parent.verticalCenter
                            wrapMode: Text.WordWrap
                        }

//                        Rectangle {
//                            id: div
//                            width: parent.width
//                            height: units.gu(0.2)
//                            color: "blue"
//                        }
                    }
                }
            }


            Row {
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.bottom: parent.bottom
                anchors.bottomMargin: units.gu(1)
                spacing: units.gu(2)

                Button {
                    text: "Search"
                    onClicked: {
                        console.log("btModel.running: " + btModel.running )
                        var list = []
                        devlist.model = list;
                        chat.searchForDevices()
                        indicator.running = true
                    }
                }

                Button {
                    text: "Stop search"
                    onClicked: {
                        console.log("Stop is clicked!")
                        chat.stopSearch();
                        indicator.running = false
                    }
                }

                Button {
                    text: "Disconnect"
                    onClicked: {
                        console.log("Disconnect is clicked!")
                        chat.disconnect();
                    }
                }
            }
        }
    }
}

整个应用的代码在:https://github.com/liu-xiao-guo/btchat


作者:UbuntuTouch 发表于2016/7/5 14:19:11 原文链接
阅读:215 评论:0 查看评论

Read more
UbuntuTouch

对于一些Ubuntu Geek来说,他们有时更喜欢使用命令行的方式来启动在手机中的应用,并通过这样的方式来调试我们的应用.我们可以通过如下的方式进入到手机的shell中:




我们可以在手机中寻找如下的预先安装好的应用:



对于这里面的应用来说,我们可以通如下的命令来启动我们的应用:


phablet@ubuntu-phablet:~$ ubuntu-app-launch messaging-app

这样我们就可以启动messaging应用了.

当然,我们也可以启动我们已经安装过得应用:


我们可以通过click list命令来查看所有已经安装的应用.我们可以通过如下的方式来启动该应用

$ ubuntu-app-launch `ubuntu-app-triplet com.ubuntu.camera`



在上面的例子中,我们可以启动camera应用.


作者:UbuntuTouch 发表于2016/7/6 17:33:21 原文链接
阅读:206 评论:0 查看评论

Read more
UbuntuTouch

Canonical公司于最近2016年4月发布了一个新的Ubuntu 16.04系统,并且这个系统是长期支持版(LongTerm Support - LTS).它一如既往地支持debian安装包,但同时它也支持最新的snap安装包.snap安装包是Canonical公司最新发布的一种安装包的格式,它甚至可以在其它的Linux发行版上安装.更多的信息可以在我们的官方开发者网站:https://developer.ubuntu.com/en/desktop/http://snapcraft.io/查看.


1)什么是snap?


一个snap包:
  • 是一个基于squashFS文件系统的文件.它包含应用代码及包含有一个应用特有的叫做snap.yaml的metadata文件.它含有一个只读的文件系统.一旦安装,它会创建一个应用特有可以写的区域,任何其它的应用都不可以访问这个区域
  • 它完全独立于系统.在snap包里,它包含了它可以运行的所有需要的库及runtime(比如python或Java等),并且它可以通过网路更新,同时也可以退回到上一个版本,而不影响系统的其它部分的运行
  • 它是受限的.通过安全机制,它具有沙箱的属性,不可以随意访问外部资源,并和系统的其它部分进行隔离.它可以通过良好设计的安全策略和其它的snap进行交交互.


2)16.04桌面支持


如果大家还没自己的16.04的桌面系统,大家可以在地址下载最新的16.04的系统.



从上面的图中,我们可以看出来在16.04的桌面中支持两种格式的安装包:snapdebian.另外我们可以看出,snap包每个安装的应用都是自成一体:每个snap应用包含运行所需要的任何依赖(dependencies);同时我们可以看出每个snap应用都是互相隔离的(请注意OS也是一个snap).和debian包相比较,我们可以看出来每个debian应用的安装依赖于其它包的安装;debian应用之间可以不受限制地互相访问而造成安全问题;删除其中的一个debian应用或包可能导致其它的应用不可以正常运行.相比较而言,不同的snap应用可以安装同样一个软件的不同版本(比如一个安装python 2.7,另外一个应用安装python 3.3)而不造成任何的干扰.从理论上讲,一个snap应用可以安装到任何一个Linux的发行版上,因为它不依赖于操作系统及其发布版本.这对于应用的维护来说是非常好的.

目前在如下的Linux发行版上支持snap包的安装.大家如果有兴趣的话,可以试一下.大家甚至可以直接从源代码编译在它上面运行的snapd环境.



Canonical公司目前正在号召全社区把应用移植成为snap包,并最终把操作系统变为ubuntu core系统,从而打造最安全的操作系统及良好的应用维护.

对于一个All-snap Ubuntu core系统来说(如上面的右图所示),它可以分为两个逻辑部分:

  • 只读的最基本的系统
    • 这部分包括配置文件,标准目录,库,工具及核心的服务(比如network services, libc, systemd及其它).系统的这部分是只读的,里面的每个元素不可以被分别更新.这个被称之为"system-image".在一个系统中,这种image可以达到两个及以上.这些最基本的系统是一种root filesystem的形式出现的.在启动后它们之间可以互相roll back,也即如果一个系统启动有问题,可以自动切换到先前的或指定的系统image去.这个部分也是通过snap打包来实现的.
  • 可写的snap应用及在其之上的架构(framework).它们利用上面的系统所提供的服务达成.

3)安装


为了能够使得在Ubuntu 16.04的系统上运行snap应用,我们必须做一些安装.我们直接使用Ctrl+Alt+T打开terminal:

$ sudo apt update
$ sudo apt install snapd
$ sudo apt install snapcraft

在我们的Ubuntu 16.04系统中,我们必须打开universe,这样我们可以在以后的开发中安装snapcraft工具了.snapcraft是为了我们能够编译一个snap项目而必须的一个工具,尽管在运行时并不需要.它位于下图所示的universe channel中.这个可以在我们的Ubuntu系统中的设置中进行选择:

  

你也可以通过命令行的方式来添加这个universe的仓库.

在这里,我们简单地介绍一下所使用的术语:
  • snapd:它是一个帮我们管理snap安装,卸载及通过事务性更新(transactional update)的一个环境.同时也帮我对老的版本的snap进行垃圾回收(garbage collection)
  • snapcraft:这是一个帮我们打包一个snap应用的工具.snapcraft.yaml是用来定义如何把一个应用打包为snap包的yaml文件格式.snapcraft工具利用它打包.

然后,你就可以在我们的terminal中安装及运用一个我们所需要的应用:

$ sudo snap install ubuntu-calculator-app
$ ubuntu-calculator-app.calculator

我们可以在我们的电脑的dash中直接运行我们所安装的应用:





如果大家想安装更多的应用的话,可以直接到我们桌面系统的应用商店进行安装:



当一个应用被成功安装以后,我们也可以通过如下的命令来查看:

liuxg@liuxg:~/snappy/desktop/rssreader$ snap list
Name                   Version               Rev  Developer      Notes
hello-world            6.3                   27   canonical      -
hello-world-cli        0.1                   x1                  -
hello-xiaoguo          1.0                   x2                  -
rssreader-app          1.0                   x2                  -
snappy-debug           0.23                  22   canonical      -
telegram-sergiusens    0.9.50                x1                  -
test-license           0.1                   x1                  -
ubuntu-calculator-app  2.1+snap3             5    ubuntucoredev  -
ubuntu-core            16.04+20160531.11-56  122  canonical      -
webcam-webui           1.0                   x1                  -
我们可以从上面看出来所有已经被成功安装过的应用.每个应用被安装后,就有一个自己的Version号码,同时也有一个Rev号码.对于从Ubuntu Store商店里安装后的应用,这个Rev是一个数字号码,比如上面的ubuntu-calculator-app应用的Rev号码是5,单对于其它的不是从商店安装的应用来说,这个号码不是一个数字.

一般来说,我们安装snap应用时在默认的情况下,我们是从stable channel进行安装的.我们可以通过如下的命令从beta/edge channel进行安装:

$ snap install hello --channel-beta

或:

$ snap refresh hello --channel=beta
Name    Version   Rev   Developer   Notes
hello   2.10.1    29    canonical   -
hello  (beta) installed

从上面我们可以看出来,calculator应用也是在里面的.如果大家想知道这个应用是如何实现的,请参考源码

https://code.launchpad.net/~dpm/ubuntu-calendar-app/snap-all-things

细心的开发者也许会发现,这个应用实际上是使用了同样一个和Ubuntu手机一样的代码.没有做任何的改变.从某种意义上讲,Ubuntu实现了真正意义上的融合(Convergence)应用设计.在为了,我们只需要一个应用的snap包,它就可以直接运行于不同屏幕尺寸上,并自动适配屏幕尺寸从而得到最佳的显示效果.比如在我们的另外一个教程中"如何把一个qmake的Ubuntu手机应用打包为一个snap应用",它展示了如何把一个手机的应用转换为一个可以在桌面系统运行的snap应用.

从另外一个角度上讲,这个snap应用时间上可以部署到任何一个支持snap包安装的Linux的发行版上,只要有它支持snap包,并且它将不依赖于操作系统的版本发布.维护性应该是非常好的.

在通常情况下,一个snap应用每天会在后台检查最新的snap版本,并自动安装.当然,我们也可以通过如下的命令来更新我们的snap应用:
$ snap refresh <snap name>
我们也可以通过如下的命令来rollback到以前的版本(从snapd 2.11版本开始支持)
$ snap revert <snap name>

我们怎么通过命令行来查找我们所需要的snap应用呢?

liuxg@liuxg:~$ snap find calculator
Name                   Version    Developer      Notes  Summary
ubuntu-calculator-app  2.1+snap3  ubuntucoredev  -      Ubuntu Calculator application for the Unity 7 desktop

目前find命令只支持搜索在stable channel的应用.我们可以通过上面的命令来查找在商店里应用名字含有calculator的应用.我们可以通过如下的命令来寻找所有在商店里发布的snap应用:

liuxg@liuxg:~$ snap find 
Name                       Version                    Developer             Notes    Summary
ab                         1.0                        snappy-test           -        Test snap with shortest name
ag-mcphail                 1.0.1                      njmcphail             -        The Silver Searcher - mcphail's build and upstream git version
alsa-utils                 1.1.0-1                    woodrow               -        Utilities for configuring and using ALSA
apktool                    2.1.1                      ligboy                -        A tool for reverse engineering 3rd party, closed, binary Android apps.
...

当然,我们也可以通过如下的方法找寻到我们所需要的应用:
liuxg@liuxg:~$ snap find | grep hello
hello                      2.10                       canonical             -        GNU Hello, the "hello world" snap
hello-bluet                0.1                        bluet                 -        Qt Hello World example
hello-huge                 1.0                        noise                 -        a really big snap
hello-snap                 0.01                       muhammad              -        GNU hello-snap, the "Hello, Snap!" snap

4)删除一个snap应用


刚才我们已经成功安装了一个snap应用到我们的桌面系统中.我们现在可以通过如下的命令来删除该应用.我们首先在命令行中显示已经被安装的应用:

liuxg@liuxg:~$ snap list
Name                   Version               Rev  Developer      Notes
hello-world            6.1                   26   canonical      -
rssreader              1.0                   x1                  devmode
rssreader-app          1.0                   x2                  -
snaptest               1                     x1                  devmode
snaptest-app           1                     x3                  devmode
ubuntu-calculator-app  2.1+snap3             5    ubuntucoredev  -
ubuntu-core            16.04+20160531.11-56  122  canonical      -
webcam-webui           1                     x1                  -

在上面,我们看到已经安装了ubuntu-calculator-app应用.我们可以通过如下的方法来删除它.
liuxg@liuxg:~$ sudo snap remove ubuntu-calculator-app
[sudo] password for liuxg: 

Done

重新显示我们已经安装的snap应用列表:

liuxg@liuxg:~$ snap list
Name           Version               Rev  Developer  Notes
hello-world    6.1                   26   canonical  -
rssreader      1.0                   x1              devmode
rssreader-app  1.0                   x2              -
snaptest       1                     x1              devmode
snaptest-app   1                     x3              devmode
ubuntu-core    16.04+20160531.11-56  122  canonical  -
webcam-webui   1                     x1 
显然我们再也找不到ubuntu-calculator-app应用了.


5)在哪里找到安装的文件


当我们把一个应用到我们的系统中后,我们可以通过如下的命令来查看在我们的系统中所安装的所有的snap应用:

liuxg@liuxg:~$ snap list
Name                   Version               Rev  Developer      Notes
hello-world            6.1                   26   canonical      -
rssreader              1.0                   x1                  devmode
rssreader-app          1.0                   x2                  -
snaptest               1                     x1                  devmode
snaptest-app           1                     x3                  devmode
ubuntu-calculator-app  2.1+snap3             5    ubuntucoredev  -
ubuntu-core            16.04+20160531.11-56  122  canonical      -
webcam-webui           1                     x1                  -

对于一些开发者来说,snap的一些命令可能比较陌生.我们可以通过如下的方法来得到帮助:

$ snap --help      # Or use 'snap <command> --help' for help on a specific command

安装好我们的应用后,我们可以在如下的路径找到我们的snap安装文件:

liuxg@liuxg:/var/lib/snapd/snaps$ ls
hello-world_26.snap    rssreader_x1.snap     snaptest-app_x3.snap          ubuntu-core_122.snap
rssreader-app_x1.snap  snaptest-app_x1.snap  snaptest_x1.snap              webcam-webui_x1.snap
rssreader-app_x2.snap  snaptest-app_x2.snap  ubuntu-calculator-app_5.snap
我们可以通过如下的方法查看系统中的mount的情况:
liuxg@liuxg:~$ mount | grep calculator
/var/lib/snapd/snaps/ubuntu-calculator-app_5.snap on /snap/ubuntu-calculator-app/5 type squashfs (ro,relatime)

从上面我们可以看出来,实际上我们是把/var/lib/snapd/snaps/ubuntu-calculator-app_5.snap文件通过mount的方法使之可以在/snap/ubuntu-calculator-app/5目录中可以看见.一般来说,一个snap应用在被成功安装后,它位于/snap/$name/$version/目录中.

liuxg@liuxg:/snap/ubuntu-calculator-app/5$ tree -L 2
.
├── bin
│   └── calculator
├── build
│   └── ubuntu-calculator-app
├── command-calculator.wrapper
├── etc
│   ├── apparmor.d
│   ├── dbus-1
│   ├── default
│   ├── drirc
│   ├── fonts
│   ├── gps.conf
│   ├── gss
│   ├── init
│   ├── init.d
│   ├── ldap
│   ├── pki
│   ├── pulse
│   ├── ucf.conf
│   ├── X11
│   └── xdg
├── lib
│   ├── systemd
│   └── x86_64-linux-gnu
├── meta
│   ├── gui
│   └── snap.yaml
├── usr
│   ├── bin
│   ├── lib
│   └── share
└── var
    └── lib

细心的读者也许已经发现,在被mount的目录中的文件就像另外一个Linux的安装文件结构.它实际上是把这个calculator所需要的所有需要的文件安装到同样的一个根目录中,从而摆脱对系统文件的任何需求.理论上讲,我们的应用不会因为系统的升级或版本的变化而造成不能运行的情况.这对于一些软件开发商来说无疑是一个天大的利好!我们今天设计好的软件,在Ubuntu升级为未来的20.4时或其它版本时,我们不需要做任何的修改.这样做有一个非常大的好处就是我们应用的设计完全摆脱了对发行版本的依赖.当然,我们也可以把我们的应用部署到其它的任何一个支持snap包安装的Linux发行版上,它也可以运行得非常好.我们不需要考虑它到底运行的是什么版本的Linux系统及什么版本的发行.

一个snap系统包含一系列的snap应用.每个应用都是独立的,并且都是只读的.每个snap应用都通过下节中描述的interface进行交流.


snap系统将所有的snap应用在/snap/bin中呈现给我们使用.你的系统$PATH中含有这个路径,所有你可以在任何的位置启动你的snap应用:

liuxg@liuxg:/snap/bin$ echo $PATH
/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
当然对于一个snap应用来说,它的snap文件包的大小可能也是非常大的.
-rw-r--r--  1 liuxg liuxg 122M 7月  12 12:00 ubuntu-calculator-app_2.1+snap3_amd64.snap

从上面可以看出来,我们的snap包的大小达到122M.如果我们想查看我们所在包里面的内容,我们可以通过如下的命令来实现:

 $ unsquashfs -l ubuntu-calculator-app_2.1+snap3_amd64.snap | less
squashfs-root
squashfs-root/bin
squashfs-root/bin/calculator
squashfs-root/command-calculator.wrapper
squashfs-root/etc
squashfs-root/etc/X11
squashfs-root/etc/X11/Xreset
squashfs-root/etc/X11/Xreset.d
squashfs-root/etc/X11/Xreset.d/README
squashfs-root/etc/X11/Xresources
squashfs-root/etc/X11/Xresources/x11-common
squashfs-root/etc/X11/Xsession
squashfs-root/etc/X11/Xsession.d
squashfs-root/etc/X11/Xsession.d/20x11-common_process-args
squashfs-root/etc/X11/Xsession.d/30x11-common_xresources
squashfs-root/etc/X11/Xsession.d/35x11-common_xhost-local
squashfs-root/etc/X11/Xsession.d/40x11-common_xsessionrc
squashfs-root/etc/X11/Xsession.d/50x11-common_determine-startup
squashfs-root/etc/X11/Xsession.d/60x11-common_localhost
squashfs-root/etc/X11/Xsession.d/60x11-common_xdg_path
...

我们也可以直接通过如下的命令来得到在snap包中所有的文件:

$ unsquashfs ubuntu-calculator-app_2.1+snap3_amd64.snap
$ cd cd squashfs-root
# Hack hack hack
$ snapcraft snap

我们可以通过最后的命名snapcraft snap来重新打包我们的应用.

我们来看一看我们安装后的应用所占的空间大小:

375M	./ubuntu-calculator-app/

也就是说一个应用安装后的空间大小是350M大小.当然这也依赖于我们所安装的应用类型.针对我们的ubuntu-calculator-app来说,我们在包里把我们所需要的Qt库及其它需要的任何东西都打入到包里面了.对于其它的任何python应用来说,也是同样的,我们可以把python版本所需要的任何库都打入到我们的包里面.我们根本不需要担心它会不会对其它的应用造成任何的影响.


6)发布我们的应用到商店


我们可以很方便地把我们已经开发好的应用通过"My Apps"发布到我们的应用商店.在上传我们的应用时,我们一定要记得选择"Ubuntu Core"作为商店来上传.




为了上传一个新的snap应用到商店,我们只需要填入我们所需要的metadata信息及上传我们开发的snap文件即可:



如果您是一个设备制造商或运营上,您甚至可以创建属于自己的store.一个简单的制造商店的snap应用可以在地址找到.
我们也可以通过命令行的方式来发布我们的应用:
$ snapcraft login
我们首先通过上面的命令登陆,让后使用如下的命令上传应用:
$ snapcraft register
$ snapcraft upload
在我们上传一个应用时,我们必须先注册一个snap的package名称.具体的操作可以参阅文章"Learn to make a snap"中的Store一节.
最后我们通过如下的命令退出:
$ snapcraft logout




7)受限的snap应用



当一个snap应用被安装后,在运行时,它被置于一个受限的安全的沙箱之中,并且每个应用都是互相隔离的.在默认的情况下,每个snap包中的每个应用都可以互相访问对方,并协同工作.但是,如果它访问其它的不在自己包里的应用或其它资源,它将是受限的.

实现这个安全沙箱的技术叫做AppArmorseccomp及device cgroups.每个应用都有自己的/tmp目录,devpts等.这个受限的设置是由一个叫做snapcraft.yaml的项目文件所定义的.在这个文件中,snap申明它想要访问的资源,系统将会为它生产相应的限制.关于snapcraft.yaml的详细介绍,我们会在以后的文章中逐步介绍.

每个snap应用都有自己受限的文件目录可以访问.我们可以通过安装在store里的hello-world应用来查看这些目录:

$ sudo snap install hello-world
$ hello-world.env | grep SNAP

liuxg@liuxg:~$ hello-world.env | grep SNAP
SNAP_USER_COMMON=/home/liuxg/snap/hello-world/common
SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:
SNAP_COMMON=/var/snap/hello-world/common
SNAP_USER_DATA=/home/liuxg/snap/hello-world/27
SNAP_DATA=/var/snap/hello-world/27
SNAP_REVISION=27
SNAP_NAME=hello-world
SNAP_ARCH=amd64
SNAP_VERSION=6.3
SNAP=/snap/hello-world/27
从上面可以看出,我们的应用是可以访问上面的$SNAP_COMMON目录,它对于某个snap应用的所有版本都是一样的.$SNAP_USER_DATA 目录里的数据是可以被我们的应用访问的.另外对于$SNAP_DATA里的数据,需要使用sudo才可以访问.一般来说,作为daemon的service的snap应用是可以访问这个目录里的数据的,因为它们具有sudo的权限.

由于在snap系统中,每个应用的运行是受限的.每个snap应用想访问沙箱以外的资源(或者让自己的资源暴露给其它的snap),它必须要使用interface.interface让我们可以分享一个snap的资源,和其它的snap进行交互及访问我们想得到的硬件资源.一个interface定义了两端之间的交互规则.在两端被称之为plug及slot.我们也可以理解slot为提供放(provider),而plug为消费方(consumer).我们可以通过自动或手动的方式来把plug和slot一起联系起来.当然,我们也可以删除这种连接.关于如何手动建立这种连接,请参阅我的文章"WebCam snap应用实例".我们可以使用诸如如下的命令来建立一个手动的interface plug及slot的连接:

$ sudo snap connect webcam-webui:camera ubuntu-core:camera  



有了interface,我们就可以把两个snap应用连接起来(请注意ubuntu core自己也是一个snap).我们可以通过interface来连接系统OS来分享共同的资源或一些服务(service)比如OpenGL.作为一个例子,当我们使用snapcraft来生产我们想要的snap文件时,我们想要我们的snap应用最终能够访问我们用户的$HOME文件目录.我们可以通过如下的命令来查看我们已经存在的plug及slot.

liuxg@liuxg:~$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app,snaptest-app
:locale-control      -
:log-observe         -
:modem-manager       -
:mount-observe       -
:network             -
:network-bind        webcam-webui
:network-control     -
:network-manager     -
:network-observe     -
:opengl              rssreader-app,snaptest-app,ubuntu-calculator-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app,snaptest-app,ubuntu-calculator-app
:x11                 -

上面显示了在我的电脑系统中每个snap应用所定义的plug.在上面的左边显示的所有的slot其实是OS snap (ubuntu-core)的尽管显示的很简捷.右边显示的是每个应用所定义的plug.

关于interfaces的更详细的介绍可以参阅我们的文档"Interfaces".在文章里,它详细地介绍每个plugs.当我们的应用需要访问到我们所需要的资源时,在我们的snapcraft.yaml项目文件中,我们必须申明这个权限,这样我们的应用就可以访问到我们所需要的资源.比如,针对我们的snap,如果我们想要访问$HOME目录时,我们可以在snapcraft.yaml中这样定义:

name: foo
apps:
  bar:
    command: bin/bar
    plugs: [ home, unity7 ] 

在这里,home及unity7和ubuntu core直接的连接是自动完成的.我们可以在文档Snaps interfaces看到所有的interface及它们是否可以自动连接.有了这样的plugs的定以后,这样我们的snap应用就可以访问到$HOME目录了.否则我们就可能在/var/log/syslog文件中发现denied错误信息.更多关于安全的介绍可以参阅文章"Snap security policy and sandboxing".

另外我们值得指出的是:如果我们想我们的应用还是像我们以前在ubuntu的桌面上运行而不受snap安全机制的限制,我们可以使用如下的命令来安装我们的应用:

$ sudo snap install <package.snap> --devmode

就像上面指出的那样,这是一种在developer mode下的开发.它可以让开发者在起始开发应用时放开安全问题(不受限制)大胆开发.在发布应用时,我们再进行安全的调试.更多这方面的介绍,我们可以参阅文章"Learn to make a snap"或文章"helloworld Snap例程".

我们必须注意的是

Snaps can be uploaded to the edge and beta channels only

关于snapcraft.yaml的知识,我们会在以后的章节中详细介绍,所以大家先不要着急!
虽然每个应用在每次的安装的过程中(比如在upgrade时),都会生产一个新的版本的文件目录,但是有些文件数据在不同的版本之间是共同的,它们在不同的版本运行时是不会改变的的,比如如下的这些目录:

/var/snap/<name>/current/  ← $SNAP_DATA is the versioned snap data directory
/var/snap/<name>/common/   ← $SNAP_COMMON will not be versioned on upgrades

总结上面所说的,每个应用和OS及其它应用直接的交互是通过如下的方式进行的:



另外,我们可以使用$ snap interfaces的如下命令得到更多的信息:

$ snap interfaces <snap> to find the slots offered and plugs used by the specified snap.
$ snap interfaces <snap>:<slot or plug> for details of only the specified slot or plug.
$ snap interfaces -i=<interface> [<snap>] to get a filtered list of plugs and/or slots.
比如,我们使用如下的命令可以得到该应用的所有的plugs:
liuxg@liuxg:~$ snap interfaces telegram-sergiusens
Slot           Plug
:home          telegram-sergiusens
:network       telegram-sergiusens
:network-bind  telegram-sergiusens
:unity7        telegram-sergiusens
当一个应用被更新后,他先前版本的所有的writable区域里的数据(SNAP_USER_DATA及SNAP_DATA)将被自动拷入新的版本的应用中,并被新的版本所使用.







7)如何得到snap帮助


就想我们上面所写的那样,我们可以通过如下命令来得到snap的帮助:

liuxg@liuxg:~$ snap --help
Usage:
  snap [OPTIONS] <command>

The snap tool interacts with the snapd daemon to control the snappy software platform.


Application Options:
      --version  print the version and exit

Help Options:
  -h, --help     Show this help message

Available commands:
  abort        Abort a pending change
  ack          Adds an assertion to the system
  change       List a change's tasks
  changes      List system changes
  connect      Connects a plug to a slot
  create-user  Creates a local system user
  disconnect   Disconnects a plug from a slot
  find         Finds packages to install
  help         Help
  install      Install a snap to the system
  interfaces   Lists interfaces in the system
  known        Shows known assertions of the provided type
  list         List installed snaps
  login        Authenticates on snapd and the store
  logout       Log out of the store
  refresh      Refresh a snap in the system
  remove       Remove a snap from the system
  run          Run the given snap command
  try          Try an unpacked snap in the system

针对每个snap下面的命令,我们可以通过如下的方式来得它的帮助信息:

liuxg@liuxg:~$ snap install -h
Usage:
  snap [OPTIONS] install [install-OPTIONS] <snap>

The install command installs the named snap in the system.

Application Options:
      --version        print the version and exit

Help Options:
  -h, --help           Show this help message

[install command options]
          --channel=   Use this channel instead of stable
          --edge       Install from the edge channel
          --beta       Install from the beta channel
          --candidate  Install from the candidate channel
          --stable     Install from the stable channel
          --devmode    Install the snap with non-enforcing security

通过上面的方法,我们可以对snap命令有更深的理解.
如果大家对开发snap应用感兴趣,但是希望得到别人的帮助,大家可以向snapcraft@lists.snapcraft.io Mailinglist发邮件来参入讨论.同时也可以通过如下的在freenode上的Snappy channel来参入讨论.我们有很多的专家及社区的牛人帮你回答你的问题.另外,我们也可以在AskUbuntu上提出我们的问题.如果大家对参加我们的playpen开发,可以在gitter上参入我们的讨论并交流.更多交流渠道,请参阅我们的连接:https://developer.ubuntu.com/en/snappy/support/




8)如编译一个snap应用


如果大家已经有一个snap的项目,你只需要:
  • 安装snapcraft.请参阅文章的开始部分
  • 在项目的根目录下,直接键入"snapcraft"即可.在项目的根目录下通常含有snapcraft.yaml文件或.snapcraft.yaml文件.
Canonical公司已经号召很多的全球开发者开发snap应用.我们已经把已经开发好的应用放在如下的仓库里了.如果大家对这个感兴趣,请安装如下的指令来下载这些应用作为参考:

$ git clone https://github.com/ubuntu/snappy-playpen.git
$ cd snappy-playpen

目前已经有如下的项目可以供我们参考:

atom/               idea/                openttd/         tinyproxy/
cloudfoundry-cli/   imagemagick-edge/    plank/           tyrant-unleashed-optimizer/
consul/             imagemagick-stable/  qcomicbook/      ubuntu-clock-app/
dcos-cli/           keepassx/            qdriverstation/  ubuntukylin-icon-theme/
deis-workflow-cli/  kpcli/               ristretto/       vault/
dosbox/             leafpad/             scummvm/         vlc/
ffmpeg/             minetest/            shotwell/        wallpaperdownloader/
galculator/         moon-buggy/          smplayer/        youtube-dl/
gitter-im/          mpv/                 snap-template/
heroku/             openjdk-demo/        snaptest/

我们可以直接进入到每个项目的根目录下,键入如下的命令即可:
$ snapcraft
当项目被成功编译完后,我们可以直接在项目的根目录下找到一个扩展名为.snap的文件.这就是我们所需要的snap安装文件.我们可以参照我们上面讲述的方法来安装这个应用.

如果想清除一个snap应用在编译过程中的文件,我们可以打入如下的命令:

$ snapcraft clean

更多关于snapcraft的知识可以参阅它的帮助:

liuxg@liuxg:~$ snapcraft --help
snapcraft

Usage:
 ...

The available commands are:
  help         Obtain help for a certain plugin or topic
  init         Initialize a snapcraft project.
  list-plugins List the available plugins that handle different types of part.
  login        Authenticate session against Ubuntu One SSO.
  logout       Clear session credentials.
  register     Register the package name in the store.
  tour         Setup the snapcraft examples tour in the specified directory,
               or ./snapcraft-tour/.
  upload       Upload a snap to the Ubuntu Store.

The available lifecycle commands are:
  clean        Remove content - cleans downloads, builds or install artifacts.
  cleanbuild   Create a snap using a clean environment managed by lxd.
  pull         Download or retrieve artifacts defined for a part.
  build        Build artifacts defined for a part. Build systems capable of
               running parallel build jobs will do so unless
               "--no-parallel-build" is specified.
  stage        Stage the part's built artifacts into the common staging area.
  prime        Final copy and preparation for the snap.
  snap         Create a snap.

Parts ecosystem commands
  update       Updates the parts listing from the cloud.
  define       Shows the definition for the cloud part.
  search       Searches the remotes part cache for matching parts.

Calling snapcraft without a COMMAND will default to 'snap'

在snapcraft打包的过程中,它经历如下的几个阶段:

  pull         Download or retrieve artifacts defined for a part.
  build        Build artifacts defined for a part. Build systems capable of
               running parallel build jobs will do so unless
               "--no-parallel-build" is specified.
  stage        Stage the part's built artifacts into the common staging area.
  prime        Final copy and preparation for the snap.
  snap         Create a snap.

我们可以通过snapcraft来对每个阶段分别处理来查看每一步到底做什么.比如"snapcraft pull"等.打包的顺序是按照上面所列举的顺序执行的.更多关于如何打包的过程请参阅连接http://snapcraft.io/create/

如果大家对如何开发一个Ubuntu桌面的应用感兴趣的话,可以参阅我的文章"如何把一个qmake的Ubuntu手机应用打包为一个snap应用"或"helloworld Snap例程".



9)如何运行一个snap应用


我们可以在Dash中运行我们的应用,同时,我们也可以在terminal中通过命令行的方式来启动我们的应用.在我们设计我们的snap应用时,通常我们使用一个叫做snapcraft.yaml的项目文件.我们现在来用一个例子来说明:

snapcraft.yaml

name: snaptest-app
version: 1
summary: This is a summary
description: This is the description
confinement: devmode

apps:
  test:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/snaptest
    plugs: [home,unity7,opengl]

parts:
  application:
    source: ./src
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
    stage:
      - -usr/share/pkgconfig/xkeyboard-config.pc       
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]

在上面的snapcraft.yaml文件中,我们的包的名字叫做"snaptest-app".在apps下定义了一个应用叫做"test".那么在命令行中,我们可以通过如下的方式来运行我们的最终的应用:

$ snaptest-app.test

也就是说,我们可以通过<<package-name>>.<<application-name>>来启动我们的应用.如果大家想对snapcraft有更多的了解,请参阅我们录制的视频"Snapcraft操作演示--教你如何snap一个应用

特别值得指出的是:如果一个snap的包名和应用的名称是完全一致的,那么你可以直接打入包名来运行这个应用.比如:

hello-world:

$ cat meta/snap.yaml
name: hello-world
version: 6.1
architectures: [ all ]
summary: "The 'hello-world' of snaps"
description: |
    This is a simple snap example that includes a few interesting binaries
    to demonstrate snaps and their confinement.
    * hello-world.env  - dump the env of commands run inside app sandbox
    * hello-world.evil - show how snappy sandboxes binaries
    * hello-world.sh   - enter interactive shell that runs in app sandbox
    * hello-world      - simply output text
apps:
  env:
    command: bin/env
  evil:
    command: bin/evil
  sh:
    command: bin/sh
  hello-world:
    command: bin/echo

从上面我们可以看出来,包名和应用的名称都是hello-world,那么我们可以直接使用hello-world来运行这个应用.对于其它的应用来说,我们必须使用诸如hhello-world.env来运行.


10)如何让我们的系统恢复到没有snap安装的起始状态


我们在开发snap的过程中,我们发现经常可能的情况是一个snap占用太多的空间.加之如果有多个版本被安装的话(每次安装都会生产一个新的版本),那么我们硬盘的空间占用将会变得很大.如果我们去手动去除这些版本的话,非常麻烦.我们可能需要用到umount,并且容易造成如下目录中的文件:

/var/lib/snapd/state.json

的破损,以至于我们在正常使用snap list命令不能产生我们想要的结果.那么我们如何处理这个问题呢?
我们可以在如下的地址获取我们想要的脚本:

git clone https://github.com/zyga/devtools/

让后运行如下下载的命令:

$ sudo ./reset-state

我们按照命令所指出的提示进行操作.这样我们就可以把我之前所有已经安装过的snap应用及环境都删除.我们的系统恢复到原来没有安装过任何snap的状态!


11)更多的学习资源


我们可以通过如下的方法得到我们需要的例程:

$ sudo apt update
$ sudo apt install snapd snapcraft snapcraft-examples

当然我们也可以直接把snapcraft源码下载下来:

$ git clone https://github.com/snapcore/snapcraft
我们可以在snapcraft的目录下的"demos"目录下找到所有的例程.我们可以从这些例程中学习如何使用snapcraft来打包我们的应用.

我们也可以通过如下的方式得到我们已经开发好的应用的源码:

$ git clone https://github.com/ubuntu/snappy-playpen

snapcraft tour:
$ snapcraft tour
Snapcraft tour initialized in ./snapcraft-tour/
Instructions are in the README, or http://snapcraft.io/create/#tour

我们可以在我们的home里的snapcraft-tour目录中找到我们所需要学习的例程.


我们可以利用这些资源来参考学习所有已经有的应用.



作者:UbuntuTouch 发表于2016/7/12 11:06:39 原文链接
阅读:1049 评论:0 查看评论

Read more
UbuntuTouch

我们知道Ubuntu平台提供了良好的融合(convergence)设计.通过融合设计,使得我们的同样一个应用在不需要修改任何代码的情况下,重新打包就可以运行到不同的屏幕尺寸的设备上.当然,Canonical公司最终的目的是实现snap应用运行到所有设备上,而不需要进行任何的重新打包的动作.目前Ubuntu手机上支持的应用打包格式是click包.在为了的Ubuntu SDK中,最终我们会把snap的支持加入到我们的SDK之中去.那么目前我们怎么把我们已经开发好的应用打包成为一个snap应用包,并可以成功部署到我们的电脑桌面(16.04)上呢?

如果大家对如何安装一个snap应用到16.04的桌面系统上的话,请参阅文章"安装snap应用到Ubuntu 16.4桌面系统".


1)通过Ubuntu SDK开发一个我们需要的手机应用


我们可以通过Ubuntu SDK来创建一个我们想要的项目.关于如何创建一个Ubuntu手机应用,这个不在我们的这个教程范围.如果你对如何利用Ubuntu SDK开发一个手机应用感兴趣的话,请参考我们的文章"Ubuntu 手机开发培训准备".这里将不再累述!

值得指出的是:在今天的教程中,我们将教大家如何把一个qmake的Ubuntu手机应用打包为一个snap的应用.这里,我们将利用之前我已经开发的一个项目作为例程来开始.我们在terminal下打入如下的命令:

$ git clone https://github.com/liu-xiao-guo/rssreader_snap

下载后的源码结构如下:

liuxg@liuxg:~/snappy/desktop/rssreader$ tree -L 2
.
├── snapcraft.yaml
└── src
    ├── manifest.json.in
    ├── po
    ├── rssreader
    └── rssreader.pro

从上面的结构上,我们可以看到:在src目录下的整个项目是有我们的Ubuntu SDK所创建的一个qmake项目,它有一个项目文件.pro文件.在手机上的运行情况如下:

  

同时,在我们项目的根目录下,我们发现了另外一个文件snapcraft.yaml.这个文件就是为了能够让我们把我们的qmake项目最终打包为snap应用的snap项目文件.


2)为我们的qmake项目打包


在上节中,我们已经提到了我们项目的snapcraft.yaml文件.现在我们把这个文件展示如下:

snapcraft.yaml

name: rssreader-app
version: 1.0
summary: A snap app from Ubuntu phone app
description: This is an exmaple showing how to convert a Ubuntu phone app to a desktop snap app
confinement: strict

apps:
  rssreader:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader
    plugs: [network,network-bind,network-manager,home,unity7,opengl]

parts:
  rssreader:
    source: src/
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    stage:
      - -usr/share/pkgconfig/xkeyboard-config.pc 
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]

初以乍看,这个文件和我们以往所看到的文件都不同.似乎很复杂!关于snapcraft.yaml的具体解释,我们可以参考我们的官方文档"snapcraft.yaml syntax".

在这里,我们做一个简单的解释:
  • name: 这是最终的包的名称.针对我们的情况,我们最终的snap包的名字为rssreader-app_1.0_amd64.snap
  • version: 这是我们包的版本信息.就像我们包的名称rssreader-app_1.0_amd64.snap所展示的那样.1.0是我们的版本信息
  • summary: 这是一个描述我们包信息的字符串.根据我们的设计,他只能最多长达79个字符
  • description:这是一个描述我们包的字符串.它可以比summary来得更长一些
  • confinement: 受限的种类:strict 或 devmode.当我们设置为devmode时,在安装时加上--devmode选项时,可以使得我们的应用不接受任何的安全的限制.就像我们以前在Ubuntu电脑上开发一样.我们可以随意地访问任何一个我们想要访问的目录等等
  • apps: 在这里定义我们的应用及其运行时所需要的命令.针对我们的情况,我们定义rssreader为我们的应用.当我们执行我们的应用时,我们需要使用<<包名>>.<<应用名>>来运行我们的应用.针对我们的情况,我们使用rssreader-app.rssreader来通过命令行来运行我们的应用
    • command:这是用来启动我们应用所需要的命令行.针对我们的情况:desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader.这里的desktop-launch来自我们下面的已经预先编译好的包 desktop/qt5
    • plugs:这一项定义了我们的snap应用所访问的权限.通过我们的设定,我们可以访问系统的$HOME目录及使用opengl等.更多细节请擦参阅Interfaces
  • parts: 每个part定义了我们软件所需要的部分.每个part就像一个mini的小项目.在我们编译时可以在parts的目录中分别找到对应的部分
    • rssreader: 我们定义的part的名称.它的名字可以是任何你所喜欢的名称
      • source: 定义part的源码.它可以在网站上的任何一个软件(bzr, git, tar)
      • plugin: 定义编译part所需要用到的plugin.开发者也可以拓展snapcraft的plugin.请参阅文章"Write your own plugins
      • qt-version:这个是针对Qt plugin来说的.定义Qt的版本
      • build-packages:定义在这里的每个包都是为了用来编译我们的项目的.需要安装的.它们将不会出现在最终的snap文件中
      • stage-packages:这些定义的包都要最终被打入到snap包中,并形成运行该应用所需要的文件.特别值得指出的是:我们加入了中文包ubuntu-defaults-zh-cn,从而使得我们的应用可以看见中文的显示.当然,我们包的大小也从100多兆增加到300多兆.注意这里的包都对应在我们通常ubuntu下的debian包
      • snap:定义了我们需要的或不需要的文件.在这里我们通过"-"来把一些不想要的文件剔除从而不打入到我们的包中
      • after:表明我们的这个part的编译必须是在desktop/qt5下载之后.对于有些项目,我们必须先得到一个part,并使用这个part来编译我们其它的part.在这种情况下,我们可以使用after来表明我们的先后顺序.在这里,我们也利用了其它人所开发的part desktop/qt5.我们可以在网址https://wiki.ubuntu.com/snapcraft/parts找到别人已经发布的parts.这些parts的描述也可以在地址https://wiki.ubuntu.com/Snappy/Parts找到.我们可以重复利用它们.在命令行中,我们也可以公共如下的命令来查找已经有的parts:
        • snapcraft update
        • snapcraft search
当然我们只做了一个简单的描述.更详细的关于snapcraft.yaml的描述,请参阅我们的文档"snapcraft.yaml syntax"或文档

3)编译我们的snap应用


为了编译我们的snap应用,其实非常简单.我们直接进入到我们的项目的根目录下,并打入如下的命令:

$ snapcraft

这样,我们就可以编译我们的应用,并最终生产我们所需要的.snap文件:

312M 7月  13 12:25 rssreader-app_1.0_amd64.snap

就像我们上节中介绍的那样,由于我们加入了中文字体,所以我们的应用变得非常庞大.没有字体的snap包大约为141M.
如果我们想要清楚我们的打包过程中的中间文件,我们可以打入如下的命令:

$ snapcraft clean

它将清除在parts, stage及prime目录下的所有的文件.更多关于snapcraft的介绍,可以参阅我的文章"安装snap应用到Ubuntu 16.4桌面系统".我们也可以通过如下的方式来得到它的帮助:

$ snapcraft --help


4)安装及运行我们的应用


我们可以通过如下的命令来安装我们的.snap文件:

$ sudo snap install rssreader-app_1.0_amd64.snap

我们可以通过如下的命令来运行我们的应用:

$ rssreader-app.rssreader

运行时的画面如下:



从上面可以看出来.我们没有经过任何的修改,但是我们的手机应用也可以在16.04的桌面上运行得非常好.在本应用中,它也使用了融合(Convergence)技术,从而使得我们的应用在不同的屏幕尺寸上自动适配.大家可以参阅我的文章"运用AdaptivePageLayout来做融合(convergence)设计以实现动态布局


5)安全调试


我们可以在我们的桌面系统中安装如下的软件:

$ snap install snappy-debug

如果在安装的过程中提示还需要安装其它的应用软件,我们按照提示安装即可.
接下来我们在一个terminal中打入如下的命令:

$ snappy-debug.security scanlog

根据Interfaces文档介绍,log-observe是不能自动连接的,我们需要使用如下的命令来手动建立这种连接:

$ sudo snap connect snappy-debug:log-observe ubuntu-core:log-observe

这种方法也适用于我们建立其它需要手动链接的的情况.然后在另外一个terminal中打入如下的命令:

$ rssreader-app.rssreader

那么我们可以在这个窗口看到如下的信息:



显然在我们的应用运行时产生了一些安全的问题,并提示一些上面的输出信息.我们切换到另外一个运行命令"snappy-debug.security scanlog"的窗口:



显然在这个输出窗口也显示了一些"DENIED"安全错误信息.那么这些问题是怎么来的呢?显然,这可能是我们的应用没有设置相应的plug所致.我们可以参阅我们的官方文档interfaces,并结合我们的应用.我们可以初步判断,我们可能需要network及network-bind plug.这是因为我们的应用是一个网路的应用,需要从网上抓数据.另外,在我们代码的main.cpp中,我们使用了如下的代码:

QNetworkAccessManager *MyNetworkAccessManagerFactory::create(QObject *parent)
{
    QNetworkAccessManager *nam = new QNetworkAccessManager(parent);

    QString path = getCachePath();
    QNetworkDiskCache* cache = new QNetworkDiskCache(parent);
    cache->setCacheDirectory(path);
    nam->setCache(cache);

    return nam;
}

这段代码是为了设置一个cache,从而使得我们抓过来的照片能够得到cache,进而我们不需要浪费网路资源重复获取同样的一个照片.根据我们目前所有的plugs:

liuxg@liuxg:~$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app
:locale-control      -
:log-observe         snappy-debug
:modem-manager       -
:mount-observe       -
:network             -
:network-bind        -
:network-control     -
:network-manager     -
:network-observe     -
:opengl              rssreader-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app
:x11                 -

显然,netwrok-manager是我们所需要的plug.我们可以在我们的snapcraft.yaml加入上面所述的两个plug.修改过后的snapcraft.yaml文件如下:

snapcraft.yaml

name: rssreader-app
version: 1.0
summary: A snap app from Ubuntu phone app
description: This is an exmaple showing how to convert a Ubuntu phone app to a desktop snap app
confinement: strict

apps:
  rssreader:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader
    plugs: [network,network-bind,network-manager,home,unity7,opengl]

parts:
  rssreader:
    source: src/
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]
 

重新打包我们的应用并安装.我们可以利用:
$ snap interfaces
来显示我们应用所有的plug:

liuxg@liuxg:~/snappy/desktop/rssreader$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app
:locale-control      -
:log-observe         snappy-debug
:modem-manager       -
:mount-observe       -
:network             rssreader-app
:network-bind        rssreader-app
:network-control     -
:network-manager     -
:network-observe     -
:opengl              rssreader-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app
:x11                 -
-                    rssreader-app:network-manager

在上面的最后一行,我们可以看到:
-                    rssreader-app:network-manager
这是什么意思呢?我们来重新查看连接interfaces,在那个页面里虽然目前还没有network-manager的介绍,但是我们可以看到诸如:

network-control

Can configure networking. This is restricted because it gives wide, privileged access to networking and should only be used with trusted apps.

Usage: reserved Auto-Connect: no

我们需要注意的是最下面的一句话:Auto-Connect: no.也就是说自动连接不存在.对于我们的networrk-manager的情况也是一样的.我们需要手动来连接.那么我们该如何来手动连接呢?

liuxg@liuxg:~$ snap --help
Usage:
  snap [OPTIONS] <command>

The snap tool interacts with the snapd daemon to control the snappy software platform.


Application Options:
      --version  print the version and exit

Help Options:
  -h, --help     Show this help message

Available commands:
  abort        Abort a pending change
  ack          Adds an assertion to the system
  change       List a change's tasks
  changes      List system changes
  connect      Connects a plug to a slot
  create-user  Creates a local system user
  disconnect   Disconnects a plug from a slot
  find         Finds packages to install
  help         Help
  install      Install a snap to the system
  interfaces   Lists interfaces in the system
  known        Shows known assertions of the provided type
  list         List installed snaps
  login        Authenticates on snapd and the store
  logout       Log out of the store
  refresh      Refresh a snap in the system
  remove       Remove a snap from the system
  run          Run the given snap command
  try          Try an unpacked snap in the system

我们通过上面的命令,我们可以知道snap有一个叫做connect的命令.进一步:

liuxg@liuxg:~$ snap connect -h
Usage:
  snap [OPTIONS] connect <snap>:<plug> <snap>:<slot>

The connect command connects a plug to a slot.
It may be called in the following ways:

$ snap connect <snap>:<plug> <snap>:<slot>

Connects the specific plug to the specific slot.

$ snap connect <snap>:<plug> <snap>

Connects the specific plug to the only slot in the provided snap that matches
the connected interface. If more than one potential slot exists, the command
fails.

$ snap connect <plug> <snap>[:<slot>]

Without a name for the snap offering the plug, the plug name is looked at in
the gadget snap, the kernel snap, and then the os snap, in that order. The
first of these snaps that has a matching plug name is used and the command
proceeds as above.

Application Options:
      --version            print the version and exit

Help Options:
  -h, --help               Show this help message

显然我们需要使用如下的命令来完成我们的connect工作:

$ snap connect <plug> <snap>[:<slot>]

针对我们的情况,我们使用如下的命令:

$ sudo snap connect rssreader-app:network-manager ubuntu-core:network-manager

当我们打入上面的命令后,我们重新来看我们的snap interfaces:
liuxg@liuxg:~$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app
:locale-control      -
:log-observe         snappy-debug
:modem-manager       -
:mount-observe       -
:network             rssreader-app
:network-bind        rssreader-app
:network-control     -
:network-manager     rssreader-app
:network-observe     -
:opengl              rssreader-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app
:x11                 -

显然这一次,我们已经成功地把network-manager加入到我们的应用中.我们可以重新运行我们的应用:

liuxg@liuxg:~/snappy/desktop/rssreader$ rssreader-app.rssreader 

(process:9770): Gtk-WARNING **: Locale not supported by C library.
	Using the fallback 'C' locale.
Gtk-Message: Failed to load module "overlay-scrollbar"
Gtk-Message: Failed to load module "gail"
Gtk-Message: Failed to load module "atk-bridge"
Gtk-Message: Failed to load module "unity-gtk-module"
Gtk-Message: Failed to load module "canberra-gtk-module"
qml: columns: 1
qml: columns: 2
qml: currentIndex: 0
qml: index: 0
qml: adding page...
qml: sourcePage must be added to the view to add new page.
qml: going to add the page
qml: it is added: 958
XmbTextListToTextProperty result code -2
XmbTextListToTextProperty result code -2

显然我们的应用再也没有相应的错误提示了.



6)为我们的应用加上应用图标



到目前我们的应用虽然接近完美,但是我们还是不能够在我们的dash中启动我们的应用.为此,我们在我们的应用的根目录下创建了一个setup/gui目录.在该目录下,我们创建如下的两个文件:

liuxg@liuxg:~/snappy/desktop/rssreader/setup/gui$ ls -l
total 100
-rw-rw-r-- 1 liuxg liuxg   325 7月  14 13:18 rssreader.desktop
-rw-rw-r-- 1 liuxg liuxg 91353 7月  14 11:50 rssreader.png

加入这两个文件后的项目结构为:

liuxg@liuxg:~/snappy/desktop/rssreader$ tree -L 3
.
├── setup
│   └── gui
│       ├── rssreader.desktop
│       └── rssreader.png
├── snapcraft.yaml
└── src
    ├── manifest.json.in
    ├── po
    │   └── rssreader.liu-xiao-guo.pot
    ├── rssreader
    │   ├── components
    │   ├── main.cpp
    │   ├── Main.qml
    │   ├── rssreader.apparmor
    │   ├── rssreader.desktop
    │   ├── rssreader.png
    │   ├── rssreader.pro
    │   ├── rssreader.qrc
    │   └── tests
    └── rssreader.pro

有了上面的两个文件,我们再次重新打包并安装我们的snap应用.安装完后,在我们的dash里:


我们可以找到我们的RSS Reader应用了.


7)在不需要安装的情况下运行我们的应用



我们知道在开发的过程中,我们每次安装的时候都会产生一个新的版本.这个新的安装不仅花费很多的时间,而且占用系统的空间(每个版本占用的空间比较大).在开发时,我们能否不用安装而直接使用已经在打包过程中生产的文件呢?答案是肯定的.我们可以通过:

liuxg@liuxg:~$ snap --help
Usage:
  snap [OPTIONS] <command>

The snap tool interacts with the snapd daemon to control the snappy software platform.


Application Options:
      --version  print the version and exit

Help Options:
  -h, --help     Show this help message

Available commands:
  abort        Abort a pending change
  ack          Adds an assertion to the system
  change       List a change's tasks
  changes      List system changes
  connect      Connects a plug to a slot
  create-user  Creates a local system user
  disconnect   Disconnects a plug from a slot
  find         Finds packages to install
  help         Help
  install      Install a snap to the system
  interfaces   Lists interfaces in the system
  known        Shows known assertions of the provided type
  list         List installed snaps
  login        Authenticates on snapd and the store
  logout       Log out of the store
  refresh      Refresh a snap in the system
  remove       Remove a snap from the system
  run          Run the given snap command
  try          Try an unpacked snap in the system

我们在上面的命令中,发现一个叫做try的命令.在我们运行完snapcraft命令后,snapcraft会帮我们生产相应的.snap文件.同时它也帮我们生产如下的目录:

liuxg@liuxg:~/snappy/desktop/rssreader$ ls -d */
parts/  prime/  setup/  src/  stage/

这其中有一个叫做prime的目录,其实它的里面就是我们安装snap后的文件.我们可以使用如下的方法:

$ sudo snap try prime/

通过上面的命令我们就可以把我们的snap安装到系统中.重新展示我们的snap安装目录:

liuxg@liuxg:/var/lib/snapd/snaps$ ls -al
total 287200
drwxr-xr-x 2 root root      4096 7月  15 11:13 .
drwxr-xr-x 7 root root      4096 7月  15 11:13 ..
-rw------- 1 root root  98439168 7月  14 13:10 mpv_x1.snap
-rw------- 1 root root 127705088 7月  14 17:30 photos-app_x1.snap
lrwxrwxrwx 1 root root        42 7月  15 11:13 rssreader-app_x1.snap -> /home/liuxg/snappy/desktop/rssreader/prime
-rw------- 1 root root     16384 7月  13 15:53 snappy-debug_22.snap
-rw------- 1 root root  67899392 7月  13 12:26 ubuntu-core_122.snap

我们看见其实就是一个软链接.通过这样的方法,我们很快地部署了我们的snap应用.同时避免每次安装时各个不同版本之间带来的不同安装.snap包实际上是一个使用squashfs打包而生产的.通过snap try,我们不需要创建一个squashfs文件.他的另外一个好处是我们可以随时修改这个snap的应用的内容,这是因为它本身是read-write的.我们可以甚至加上--devmode选项来取消应用对安全的限制.


8)利用devmode免除在开发时的安全考虑



我们知道在开发的过程中,我们有时不知道需要使用哪写plug来保证我们的软件正常运行.这个时候,我们可以在我们的snapcraft中定义修改我们的confinement使之成为devmode:

snapcraft.yaml


name: rssreader-app
version: 1.0
summary: A snap app from Ubuntu phone app
description: This is an exmaple showing how to convert a Ubuntu phone app to a desktop snap app
confinement: devmode

apps:
  rssreader:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader
    plugs: [network,network-bind,network-manager,home,unity7,opengl]

parts:
  rssreader:
    source: src/
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]
 

请注意上面的这一行:

confinement: devmode

当我们这样设计后,plugs里定义的所有的plug将不起任何的作用(我们甚至可以把它们全部删除).在我们安装我们的snap时,我们使用如下的命令:

$ sudo snap install rssreader-app_1.0_amd64.snap --devmode

注意我们上面加入--devmode选项.它表明,我们的应用将不受任何的安全沙箱的限制.它可以做它任何喜欢做的事情.在运行的过程中将不再生产任何的安全错误的提示.这样的设计对于我们开发者来说,我们可以很快地开发出我们所需要的应用,而不用先考虑我们的安全问题.












作者:UbuntuTouch 发表于2016/7/13 14:37:48 原文链接
阅读:673 评论:0 查看评论

Read more
UbuntuTouch

今天我把我们global同事的一个视频传到我们的youku网站上了.请大家观看如何从零开始来打包一个snap应用.大家如果有什么问题,请在文章的下面进行评论.我会尽我所能来回到所有的问题.


Ubuntu Snapcraft演示

https://www.youtube.com/watch?time_continue=1&v=K0IzxsIFjJY


Ubuntu Snappy and Snap Packages | Linux Explained:

https://www.youtube.com/watch?v=0ApRUndiXKU

作者:UbuntuTouch 发表于2016/7/20 17:09:04 原文链接
阅读:177 评论:0 查看评论

Read more
UbuntuTouch

在先前的文章"如何把一个qmake的Ubuntu手机应用打包为一个snap应用"中,我们介绍了如何把一个qmake的一个Ubuntu手机应用打包为一个snap的桌面应用.在今天的教程中,我们将展示如何把一个cmake的Ubuntu手机项目转换为一个snap的桌面应用.



1)通过Ubuntu SDK开发一个我们需要的手机应用



我们可以通过Ubuntu SDK来创建一个我们想要的项目.关于如何创建一个Ubuntu手机应用,这个不在我们的这个教程范围.如果你对如何利用Ubuntu SDK开发一个手机应用感兴趣的话,请参考我们的文章"Ubuntu 手机开发培训准备".这里将不再累述!


值得指出的是:在今天的教程中,我们将教大家如何把一个cmake的Ubuntu手机应用打包为一个snap的应用.这里,我们将利用之前我已经开发的一个项目作为例程来开始.我们在terminal下打入如下的命令:

$ git clone https://github.com/liu-xiao-guo/photos

下载后的源码结构如下:

liuxg@liuxg:~/snappy/desktop/photos$ tree -L 2
.
├── photos.wrapper
├── setup
│   └── gui
├── snapcraft.yaml
├── snappy-qt5.conf
└── src
    ├── app
    ├── CMakeLists.txt
    ├── manifest.json.in
    ├── photos.apparmor
    └── po

就像我们上面展示的那样,在src的目录中有一个完整的可以执行的cmake手机应用.它的项目管理文件是CMakeLists.txt.在它的根目录下有一个文件叫做snapcraft.yaml文件.这个是用来打包我们的CMake手机应用,并使之成为一个可以在我们的16.04桌面上运行的snap应用.



2)为我们的CMake项目打包


在上节中,我们已经提到了我们项目的snapcraft.yaml文件.现在我们把这个文件展示如下:


snapcraft.yaml


name: photos-app
version: 1.0
summary: Ubuntu photos app
description: |
   This is a demo app showing how to convert a cmake ubuntu phone app to a snap app

apps:
  photos:
    command: photos
    plugs: [network,home,unity7,opengl]

parts:
  photos:
    plugin: cmake
    configflags: [-DCMAKE_INSTALL_PREFIX=/usr, -DCLICK_MODE=off]
    source: src/
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    snap:
      - -usr/share/doc
      - -usr/include
  environment:
    plugin: copy
    files:
      photos.wrapper: bin/photos
      snappy-qt5.conf: etc/xdg/qtchooser/snappy-qt5.conf

在这里,我不想再累述这里的每一个字段的意思.大家可以参阅我的文章"如何把一个qmake的Ubuntu手机应用打包为一个snap应用".特别值得指出的是,在我们的这个snapcraft.yaml文件中,我们定义了一个新的environment的part(它可以是我们喜欢的任何名称).在它里面,它使用了copy plugin.它把当前目录下的photos.wrapper拷入到bin/photos下.我们的command中的执行文件是photos.在我们打包时,我们必须记住需要把我们的script变为可以执行的脚本:

$ chmod a+x photos.wrapper

在我们的项目中,因为CMake项目不含有任何的C++代码,所以我们必须使用qmlscene来启动.这个在我们的photos.wrapper中可以看到:

photos.wrapper

#!/bin/sh

ARCH='x86_64-linux-gnu'

export LD_LIBRARY_PATH=$SNAP/usr/lib/$ARCH:$LD_LIBRARY_PATH

# XKB config
export XKB_CONFIG_ROOT=$SNAP/usr/share/X11/xkb

# Qt Platform to Mir
#export QT_QPA_PLATFORM=ubuntumirclient
export QTCHOOSER_NO_GLOBAL_DIR=1
export QT_SELECT=snappy-qt5

# Qt Libs
export LD_LIBRARY_PATH=$SNAP/usr/lib/$ARCH/qt5/libs:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$SNAP/usr/lib/$ARCH/pulseaudio:$LD_LIBRARY_PATH

# Qt Modules
export QT_PLUGIN_PATH=$SNAP/usr/lib/$ARCH/qt5/plugins
export QML2_IMPORT_PATH=$SNAP/usr/lib/$ARCH/qt5/qml/photos
export QML2_IMPORT_PATH=$QML2_IMPORT_PATH:$SNAP/usr/lib/$ARCH/qt5/qml
export QML2_IMPORT_PATH=$QML2_IMPORT_PATH:$SNAP/lib/$ARCH

# Mesa Libs
export LD_LIBRARY_PATH=$SNAP/usr/lib/$ARCH/mesa:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$SNAP/usr/lib/$ARCH/mesa-egl:$LD_LIBRARY_PATH

# XDG Config
export XDG_CONFIG_DIRS=$SNAP/etc/xdg:$XDG_CONFIG_DIRS
export XDG_CONFIG_DIRS=$SNAP/usr/xdg:$XDG_CONFIG_DIRS
# Note: this doesn't seem to work, QML's LocalStorage either ignores
# or fails to use $SNAP_USER_DATA if defined here
export XDG_DATA_DIRS=$SNAP_USER_DATA:$XDG_DATA_DIRS
export XDG_DATA_DIRS=$SNAP/usr/share:$XDG_DATA_DIRS

# Not good, needed for fontconfig
export XDG_DATA_HOME=$SNAP/usr/share

# Font Config
export FONTCONFIG_PATH=$SNAP/etc/fonts/config.d
export FONTCONFIG_FILE=$SNAP/etc/fonts/fonts.conf

# Tell libGL where to find the drivers
export LIBGL_DRIVERS_PATH=$SNAP/usr/lib/$ARCH/dri

# Necessary for the SDK to find the translations directory
export APP_DIR=$SNAP

# ensure the snappy gl libs win
export LD_LIBRARY_PATH="$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH"

cd $SNAP
exec $SNAP/usr/bin/qmlscene $SNAP/share/qml/photos/Main.qml

在上面的最后一句,它显示了我们如何启动我们的应用:

exec $SNAP/usr/bin/qmlscene $SNAP/share/qml/photos/Main.qml

我们可以直接在我们的项目根目录下打入如下的命令:

$ snapcraft

它即可把我们的应用打包为我们想要的snap应用包.

关于如何安装和运行我们的应用.这里我就不再累述.请大家参阅我文章"如何把一个qmake的Ubuntu手机应用打包为一个snap应用".

下面,我们把运行的结果的截图显示如下:

 


作者:UbuntuTouch 发表于2016/7/15 14:20:31 原文链接
阅读:139 评论:0 查看评论

Read more
UbuntuTouch

[原]helloworld Snap例程

我们在很多的语言开发中都少不了使用"Hello, the world!"来展示一个开发环境的成功与否,在今天的教程中,我们也毫不例外地利用这个例子来展示snap应用是如何构建的.虽然这个例子很简单,但是在我们进入例程的过程中,我们会慢慢地发现有关snap系统的一些特性,这样可以更好地帮助我们来了解这个系统.如果大家想了解16.04的桌面对snap的支持,请参阅文章"安装snap应用到Ubuntu 16.4桌面系统".


1)环境安装


我们仿照在文章"安装snap应用到Ubuntu 16.4桌面系统"中的"安装"一节进行安装.记得一定要选上"universe"才可以安装成功.



2)从Snap商店中安装hello-world应用


我们可以通过如下的命令在Snap商店中找到这个应用:

$ sudo snap install hello-world

在安装完这个应用后,我们可以在如下的目录中找到关于这个应用的snap.yaml文件.这个文件很类似于一个项目的snapcraft.yaml文件.只是它里面没有什么parts的部分.

liuxg@liuxg:/snap/hello-world/current/meta$ ls 
gui  snap.yaml

其中snap.yaml的内容如下:

snap.yaml

name: hello-world
version: 6.3
architectures: [ all ]
summary: The 'hello-world' of snaps
description: |
    This is a simple snap example that includes a few interesting binaries
    to demonstrate snaps and their confinement.
    * hello-world.env  - dump the env of commands run inside app sandbox
    * hello-world.evil - show how snappy sandboxes binaries
    * hello-world.sh   - enter interactive shell that runs in app sandbox
    * hello-world      - simply output text
apps:
 env:
   command: bin/env
 evil:
   command: bin/evil
 sh:
   command: bin/sh
 hello-world:
   command: bin/echo

可以看出来它的格式非常接近我们的snapcraft.yaml项目文件.这里文件中指出的env,evil,sh及echo命令,我们可以在如下的目录中找到:

liuxg@liuxg:/snap/hello-world/current/bin$ ls
echo  env  evil  sh

当然,我们可以使用我们的vi编辑器来查看里面的具体的内容.整个的文件架构如下:

liuxg@liuxg:/snap/hello-world/current$ tree -L 3
.
├── bin
│   ├── echo
│   ├── env
│   ├── evil
│   └── sh
└── meta
    ├── gui
    │   └── icon.png
    └── snap.yaml


3)创建我们自己的hello-world项目


由于一些原因,我们在网上已经找不到这个项目的源码了.由于这个项目非常简单,我们可以通过我们自己的方法来重建这个项目:我们把上面的snap.yaml进行修改为我们所需要的snapcraft.yaml,并把我们所需要的文件考入到我们需哟啊的目录.这样我们就可以很容易地重构这个项目.这样做的目的是我们想通过修改这个项目来展示一些我们想看到的特性.

对于开发者来说,如果你没有一个现成的snapcraft.yaml文件供你参考的话,你可以直接使用如下的命令来生产一个模版来进行编辑:

$ snapcraft init

这样在我们的项目的根目录下就会生产一个snapcraft.yaml的样板件.我们可以利用这个文件来作为开始进行编辑:

snapcraft.yaml

name: my-snap  # the name of the snap
version: 0  # the version of the snap
summary: This is my-snap's summary  # 79 char long summary
description: This is my-snap's description  # a longer description for the snap
confinement: devmode  # use "strict" to enforce system access only via declared interfaces

parts:
    my-part:  # Replace with a part name of your liking
        # Get more information about plugins by running
        # snapcraft help plugins
        # and more information about the available plugins
        # by running
        # snapcraft list-plugins
        plugin: nil

经过我们的重构,我们终于完成了我们的第一个snap应用.目录架构如下:

liuxg@liuxg:~/snappy/desktop/helloworld$ tree -L 3
.
├── bin
│   ├── echo
│   ├── env
│   ├── evil
│   └── sh
├── setup
│   └── gui
│       └── icon.png
└── snapcraft.yaml

在这里snapcraft是我们的项目文件,它被用于来打包我们的应用.它的内容如下:

snapcraft.yaml


name: hello-xiaoguo
version: 1.0
architectures: [ all ]
summary: The 'hello-world' of snaps
description: |
    This is a simple snap example that includes a few interesting binaries
    to demonstrate snaps and their confinement.
    * hello-world.env  - dump the env of commands run inside app sandbox
    * hello-world.evil - show how snappy sandboxes binaries
    * hello-world.sh   - enter interactive shell that runs in app sandbox
    * hello-world      - simply output text
confinement: strict

apps:
 env:
   command: bin/env
 evil:
   command: bin/evil
 sh:
   command: bin/sh
 hello-world:
   command: bin/echo

parts:
 hello:
  plugin: copy
  files:
    ./bin: bin

从内容上看,它和我们之前看到的snap几乎是一样的.这里我们使用了copy plugin.如果大家想对snapcraft.yaml中的每一项搞清楚,建议大家阅读文章"Metadata YAML file".在其中,它也描述了哪些项是必须的.

我们可以通过如下的命令来查询目前已经支持的所有的plugins:

liuxg@liuxg:~/snappy/desktop/helloworld$ snapcraft list-plugins
ant        catkin  copy  gulp  kbuild  make   nil     python2  qmake  tar-content
autotools  cmake   go    jdk   kernel  maven  nodejs  python3  scons

事实上,在snapcraft下的plugin架构也是开放的,开发者可以开发自己所需要的plugin.
在我们打包我们的应用之前,我们必须确保在bin目录下的所有文件都是可以执行的:

liuxg@liuxg:~/snappy/desktop/helloworld/bin$ ls -al
total 24
drwxrwxr-x 2 liuxg liuxg 4096 7月  13 00:31 .
drwxrwxr-x 4 liuxg liuxg 4096 7月  18 10:31 ..
-rwxrwxr-x 1 liuxg liuxg   31 7月  12 05:20 echo
-rwxrwxr-x 1 liuxg liuxg   27 7月  12 05:20 env
-rwxrwxr-x 1 liuxg liuxg  274 7月  12 05:20 evil
-rwxrwxr-x 1 liuxg liuxg  209 7月  12 05:20 sh

否则的话,我们需要使用如下的命令来完成:

$ chmod a+x echo

至此,我们已经完成了一个最基本的hello-world项目.


4)编译并执行我们的应用


在我们的项目的根目录下,我们直接打入如下的命令:

$ snapcraft

如果一切顺利的话,我们可以在项目的根目录下看到如下的文件及目录:

liuxg@liuxg:~/snappy/desktop/helloworld$ tree -L 2
.
├── bin
│   ├── echo
│   ├── env
│   ├── evil
│   └── sh
├── hello-xiaoguo_1.0_all.snap
├── parts
│   └── hello
├── prime
│   ├── bin
│   ├── command-env.wrapper
│   ├── command-evil.wrapper
│   ├── command-hello-world.wrapper
│   ├── command-sh.wrapper
│   └── meta
├── setup
│   └── gui
├── snapcraft.yaml
└── stage
    └── bin

我们可以看见一个生产的snap文件.一个snap的文件名依赖于如下的规则:
<name>_<version>_<arch>.snap
在我们这个例程中,我们指定architecture为all.

当然,我们也看到了一下其它的文件目录:parts, stage及prime.这些目录是在打包过程中自动生成的.有关更多关于snapcraft的帮助信息,我们可以通过如下的命令来获得:

liuxg@liuxg:~/snappy/desktop/helloworld$ snapcraft --help
...

The available lifecycle commands are:
  clean        Remove content - cleans downloads, builds or install artifacts.
  cleanbuild   Create a snap using a clean environment managed by lxd.
  pull         Download or retrieve artifacts defined for a part.
  build        Build artifacts defined for a part. Build systems capable of
               running parallel build jobs will do so unless
               "--no-parallel-build" is specified.
  stage        Stage the part's built artifacts into the common staging area.
  prime        Final copy and preparation for the snap.
  snap         Create a snap.

Parts ecosystem commands
  update       Updates the parts listing from the cloud.
  define       Shows the definition for the cloud part.
  search       Searches the remotes part cache for matching parts.

Calling snapcraft without a COMMAND will default to 'snap'

...

在上面我们可以看出,打包一个snap应用所必须的生命周期:pullbuildstageprimesnap.对于一个snap的项目来说,它的每个part的文件可以存在于一下网站上,比如git,bzr或tar.在打包的过程中,snapcraft可以将它们自动从网上下载下来,并存于parts目录下.通过build,把每个part的安装的文件至于每个part下的一个叫做install的子目录中.在stage过程中,它分别把每个part的文件(parts/<part_name>/install)集中起来,并存于一个统一的文件架构中.prime是最终把stage中可以用打包的文件放在一起,这样我们可以在最后的过程中来通过snap过程来打包为snap包.最终的.snap包文件其实就是把prime目录下的文件进行squashfs处理.更多关于这些生命周期的描述,大家可以参阅文章"Snapcraft Overview".我们也可以参阅视频"Snapcraft操作演示--教你如何snap一个应用".

如果大家想清除这些在打包过程中的中间文件,我们可以在命令行中打入如下的命令:

$ snapcraft clean

我们可以通过如下的命令来安装我们的应用:

$ sudo snap install hello-xiaoguo_1.0_all.snap

在编译一个snap应用时,我们也可以分别对以上的步骤进行操作.在每次操作过后,我们会发现我们的目录结构将会发送改变.
$ snapcraft clean
$ snapcraft pull
$ snapcraft build
$ snapcraft stage
$ snapcraft prime
$ snapcraft snap prime/
如果我们通过上面的方法进行安装的话,每次安装都会生产一个新的版本,并且占用系统的硬盘资源.我们也可以通过如下的方式来进行安装:

$ sudo snap try prime/

这种方法的好处是,它不必要每次都生产一个新的版本.在安装时,它直接使用一个软链接的方式,把我们编译的prime目录直接进行mount.

这样我们就可以把我们的应用安装到我们的16.04的桌面系统中了.我们可以通过如下的命令来查看:

liuxg@liuxg:~/snappy/desktop/helloworld$ snap list
Name           Version               Rev  Developer  Notes
hello-world    6.3                   27   canonical  -
hello-xiaoguo  1.0                   x2              -
ubuntu-core    16.04+20160531.11-56  122  canonical  -

如果你已经看到了hello-xiaoguo的应用在上面列表中.那么恭喜你,你已经创建了人生第一个snap.从上面的列表中我们可以看出,它即有一个Version也有一个Rev版本.显然我们的hello-xiaoguo应用已经被成功安装到系统中了.那么我们怎么来运行我们的应用呢?

从我们的snapcraft.yaml文件中,我们可以看出来,我们的包名叫做hello-xiaoguo.在我们的这个包中,我们定义了四个应用:env, evil,sh及hello-world.那么我们运行它们时,我们分别需要使用如下的命令来运行:

$ hello-xiaoguo.env
$ hello-xiaoguo.evil
$ hello-xiaoguo.sh
$ hello-xiaoguo.hello-world

也即,我们使用<<包名>>.<<应用名>>格式来运行我们的应用.比如:

liuxg@liuxg:~/snappy/desktop/helloworld$ hello-xiaoguo.hello-world 
Hello World!

整个项目的源码在:https://github.com/liu-xiao-guo/helloworld-snap.另外我们特别指出的是,如果你的包的名称和应用名称是一样的,那么你只需要运行包的名称即可.比如你的包的名称是hello,你的应用的名称是hello,那么你直接在命令行中运行hello就可以运行你的应用.

我们可以使用如下的命令来查看我们的snap文件包里的内容:

$ unsquashfs -l hello-xiaoguo_1.0_all.snap

一个snap文件包其实就是一个压缩的squashfs文件.我们也可以通过如下的方式来得到包里所有的内容:
$ unsquashfs hello-xiaoguo_1.0_all.snap  
$ cd cd squashfs-root  
# Hack hack hack  
$ snapcraft snap  
这里我们就留给开发者们自己做练习.



5)Snap的运行环境


每个应用在被安装到系统的时候,系统将会为这个包里的每个应用生产一个相应的启动脚本.它们可以在如下的路径中找到:

liuxg@liuxg:/snap/bin$ ls -l
total 52
-rwxr-xr-x 1 root root 708 7月  20 15:37 hello-world
-rwxr-xr-x 1 root root 783 7月  21 15:13 hello-world-cli
-rwxr-xr-x 1 root root 683 7月  20 15:37 hello-world.env
-rwxr-xr-x 1 root root 687 7月  20 15:37 hello-world.evil
-rwxr-xr-x 1 root root 679 7月  20 15:37 hello-world.sh
-rwxr-xr-x 1 root root 743 7月  22 15:30 hello-xiaoguo.createfile
-rwxr-xr-x 1 root root 767 7月  22 15:30 hello-xiaoguo.createfiletohome
-rwxr-xr-x 1 root root 715 7月  22 15:30 hello-xiaoguo.env
-rwxr-xr-x 1 root root 719 7月  22 15:30 hello-xiaoguo.evil
-rwxr-xr-x 1 root root 747 7月  22 15:30 hello-xiaoguo.hello-world
-rwxr-xr-x 1 root root 711 7月  22 15:30 hello-xiaoguo.sh
-rwxr-xr-x 1 root root 726 7月  22 11:32 snappy-debug.security
-rwxr-xr-x 1 root root 798 7月  20 10:44 telegram-sergiusens.telegram

我们可以通过cat命令来查看这些脚本的具体的内容.
我们可以执行如下的命令:

$ hello-xiaoguo.env | grep SNAP

通过上面的指令,我们可以得到关于一个snap运行时的所有环境变量:

liuxg@liuxg:~$ hello-xiaoguo.env | grep SNAP
SNAP_USER_COMMON=/home/liuxg/snap/hello-xiaoguo/common
SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:
SNAP_COMMON=/var/snap/hello-xiaoguo/common
SNAP_USER_DATA=/home/liuxg/snap/hello-xiaoguo/x2
SNAP_DATA=/var/snap/hello-xiaoguo/x2
SNAP_REVISION=x2
SNAP_NAME=hello-xiaoguo
SNAP_ARCH=amd64
SNAP_VERSION=1.0
SNAP=/snap/hello-xiaoguo/x2

这些和snap相关的环境变量可以在我们的应用中进行引用.这些变量的介绍如下:



在我们编程的过程中,我们不必要hard-code我们的变量.比如我们可以在我们的snapcraft中引用$SNAP,它表示我们当前的snap的安装的路径.同时,我们可以看到一个很重要的目录:

SNAP_DATA=/var/snap/hello-xiaoguo/x2

这个目录是我们的snap的私有的目录.我们的snap只能向这个目录或SNAP_USER_DATA进行读写的操作.否则,将会产生安全的错误信息.比如在我们的evil脚本中:

evil

#!/bin/sh

set -e
echo "Hello Evil World!"

echo "This example demonstrates the app confinement"
echo "You should see a permission denied error next"

echo "Haha" > /var/tmp/myevil.txt

echo "If you see this line the confinement is not working correctly, please file a bug"

我们通过这个脚本向/var/tmp/中的myevil.txt写入"Haha",它会造成安全的DENIED错误信息:

liuxg@liuxg:~$ hello-xiaoguo.evil
Hello Evil World!
This example demonstrates the app confinement
You should see a permission denied error next
/snap/hello-xiaoguo/x2/bin/evil: 9: /snap/hello-xiaoguo/x2/bin/evil: cannot create /var/tmp/myevil.txt: Permission denied

另外一种查找错误的方方法是打开我们的系统的log信息(/var/log/syslog

8307 comm="fswebcam" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
Jul 19 13:27:18 liuxg kernel: [19665.330053] audit: type=1400 audit(1468906038.378:4309): apparmor="DENIED" operation="open" profile="snap.webcam-webui.webcam-webui" name="/etc/fonts/fonts.conf" pid=18307 comm="fswebcam" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
Jul 19 13:27:25 liuxg gnome-session[2151]: (nm-applet:2584): nm-applet-WARNING **: ModemManager is not available for modem at /hfp/org/bluez/hci0/dev_F4_B7_E2_CC_F0_56
Jul 19 13:27:26 liuxg kernel: [19673.182647] audit: type=1400 audit(1468906046.230:4310): apparmor="DENIED" operation="mknod" profile="snap.hello-xiaoguo.evil" name="/var/tmp/myevil.txt" pid=18314 comm="evil" requested_mask="c" denied_mask="c" fsuid=1000 ouid=1000

或直接使用如下的命令:

liuxg@liuxg:~/snappy/desktop/helloworld$ cat /var/log/syslog | grep DENIED | grep hello-xiaoguo
Jul 19 13:25:25 liuxg kernel: [19552.926619] audit: type=1400 audit(1468905925.975:4276): apparmor="DENIED" operation="mknod" profile="snap.hello-xiaoguo.evil" name="/var/tmp/myevil.txt" pid=18273 comm="evil" requested_mask="c" denied_mask="c" fsuid=1000 ouid=1000
Jul 19 13:27:26 liuxg kernel: [19673.182647] audit: type=1400 audit(1468906046.230:4310): apparmor="DENIED" operation="mknod" profile="snap.hello-xiaoguo.evil" name="/var/tmp/myevil.txt" pid=18314 comm="evil" requested_mask="c" denied_mask="c" fsuid=1000 ouid=1000

这其中最根本的原因是因为我们的snap应用不能访问不属于他自己的目录.这是snap应用最根本的安全机制.我们可以看到上面的operation项.他指出了我们具体的错误操作.
对于snap应用的另外一个安全方面的问题是seccomp violation.对于一个叫做audit的应用来说,我们可以通过如下的方式来得到它的错误信息:

$ sudo grep audit /var/log/syslog

一个seccomp violation的例子可能如下:

audit: type=1326 audit(1430766107.122:16): auid=1000 uid=1000 gid=1000 ses=15 pid=1491 comm="env" exe="/bin/bash" sig=31 arch=40000028 syscall=983045 compat=0 ip=0xb6fb0bd6 code=0x0

那么这个violation的具体的意思是什么呢?我们可以通过如下的命令来查看这个错我的信息:

$ scmp_sys_resolver 983045
set_tls

请注意上面的983045来自于我们的错误信息中的"syscall=983045".这样,我们就可以定位我们的哪些系统调用有问题.

为了说明问题,我们创建一个createfile的脚本:

createfile

#!/bin/sh

set -e
echo "Hello a nice World!"

echo "This example demonstrates the app confinement"
echo "This app tries to write to its own user directory"

echo "Haha" > $HOME/test.txt

echo "Succeeded! Please find a file created at $HOME/test.txt"
echo "If do not see this, please file a bug"

在我们的snapcraft.yaml中加入:

 createfile:
   command: bin/createfile

重新打包我们的应用:

liuxg@liuxg:~/snappy/desktop/helloworld$ hello-xiaoguo.createfile 
Hello a nice World!
This example demonstrates the app confinement
This app tries to write to its own user directory
Succeeded! Please find a file created at /home/liuxg/snap/hello-xiaoguo/x3/test.txt
If do not see this, please file a bug

在这里,我们可以看见我们已经成功地在位置/home/liuxg/snap/hello-xiaoguo/x3/生产一个文件.$HOME的定义可以通过如下的方式获得:

liuxg@liuxg:~/snappy/desktop/helloworld$ hello-xiaoguo.env | grep home
GPG_AGENT_INFO=/home/liuxg/.gnupg/S.gpg-agent:0:1
SNAP_USER_COMMON=/home/liuxg/snap/hello-xiaoguo/common
ANDROID_NDK_ROOT=/home/liuxg/android-ndk-r10e
SNAP_USER_DATA=/home/liuxg/snap/hello-xiaoguo/x3
PWD=/home/liuxg/snappy/desktop/helloworld
HOME=/home/liuxg/snap/hello-xiaoguo/x3
XAUTHORITY=/home/liuxg/.Xauthority

细心的开发者也许会发现它的定义其实和变量SNAP_USER_DATA是一样的:

liuxg@liuxg:~/snappy/desktop/helloworld$ hello-xiaoguo.env | grep SNAP
SNAP_USER_COMMON=/home/liuxg/snap/hello-xiaoguo/common
SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:
SNAP_COMMON=/var/snap/hello-xiaoguo/common
SNAP_USER_DATA=/home/liuxg/snap/hello-xiaoguo/x3
SNAP_DATA=/var/snap/hello-xiaoguo/x3
SNAP_REVISION=x3
SNAP_NAME=hello-xiaoguo
SNAP_ARCH=amd64
SNAP_VERSION=1.0
SNAP=/snap/hello-xiaoguo/x3

对于一个daemon应用来说,这个$HOME变量的值,是指向$SNAP_DATA而不是$SNAP_USER_DATA.这一点大家要记住.具体的描述可以参阅文章"Snap security policy and sandboxing".

关于security的调试是一件非常麻烦的是.庆幸的是,我们可以利用snappy-debug来帮我们调试.它可以帮我们解析我们到底是哪些地方有错误,并且帮我们解析系统调用的名称等.

$ sudo snap install snappy-debug
$ sudo /snap/bin/snappy-debug.security scanlog foo

具体的使用方法,我们可以参阅文章"Snap security policy and sandboxing".




6)创建一个可以向桌面home写入的app


在上一节的内容中,我们基本上已经看到了snap应用的安全性的一些方面.在我们的snap应用中,假如我们想向我们的桌面电脑中的home里写内容,那应该是怎么办呢?首先,我们创建一个如下的createfiletohome脚本:

createfiletohome:


#!/bin/sh

set -e
echo "Hello a nice World!"

echo "This example demonstrates the app confinement"
echo "This app tries to write to its own user directory"

echo "Haha" > /home/$USER/test.txt

echo "Succeeded! Please find a file created at $HOME/test.txt"
echo "If do not see this, please file a bug"

在上面我们向我们的用户的home目录写入我们需要的内容.如果我们重新编译并运行我们的应用,我们会发现:

liuxg@liuxg:~/snappy/desktop/helloworld$ hello-xiaoguo.createfiletohome 
Hello a nice World!
This example demonstrates the app confinement
This app tries to write to its own user directory
/snap/hello-xiaoguo/x1/bin/createfiletohome: 9: /snap/hello-xiaoguo/x1/bin/createfiletohome: cannot create /home/liuxg/test.txt: Permission denied

显然,它产生了一个错误的信息.我们不可以向我们的用户的home写入任何的内容.那么我们怎么才可以呢?

方法一: 我们使用如下的方法来安装我们的应用:

$ sudo snap install hello-xiaoguo_1.0_all.snap --devmode

注意上面的--devmode,它表明在开发的过程中,我们的snap应用忽略任何的安全问题,也就是我们的snap将和以前开发的任何应用一样,不接受snap系统所带来的任何的安全的限制.这种方式一般适合在早期开发一个snap应用时使用.等到我们把功能完善后,我们再来完善我们的安全的部分.

针对使用--devmode打包的snap来说(我们也可以在snapcraft.yaml中的confinement设为devmode),我们必须注意的是: 

Snaps can be uploaded to the edge and beta channels only


另外特别值得指出的是,如果我们的snapcraft.yaml中的confinement被定义为devmode,那个这个应用将能被"snap find"发现而进行安装.作为开发者本身,你可以通过命令"snap install --devmode"来进行安装测试,普通用户不能使用"snap find/install"来对该应用进行操作.

方法二:我们重新把我们的snapcraft.yaml改写如下:

snapcraft.yaml

name: hello-xiaoguo
version: 1.0
architectures: [ all ]
summary: The 'hello-world' of snaps
description: |
    This is a simple snap example that includes a few interesting binaries
    to demonstrate snaps and their confinement.
    * hello-world.env  - dump the env of commands run inside app sandbox
    * hello-world.evil - show how snappy sandboxes binaries
    * hello-world.sh   - enter interactive shell that runs in app sandbox
    * hello-world      - simply output text
confinement: strict

apps:
 env:
   command: bin/env
 evil:
   command: bin/evil
 sh:
   command: bin/sh
 hello-world:
   command: bin/echo
 createfile:
   command: bin/createfile
 createfiletohome:
   command: bin/createfiletohome
   plugs: [home]

parts:
 hello:
  plugin: copy
  files:
    ./bin: bin

在上面,我们在createfiletohome应用下加入了home plug.它表明我们的应用可以访问用户的home目录.在snap系统,如果一个应用想访问另外一个受限的资源或访问一个被其它应用所拥有的资源时,我们可以通过interface来实现.更多的关于snap的interface方面的知识,大家可以参阅Interfaces链接.我们可以通过如下的命令来查看所有的interface:

liuxg@liuxg:~$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                -
:locale-control      -
:log-observe         -
:modem-manager       -
:mount-observe       -
:network             -
:network-bind        -
:network-control     -
:network-manager     -
:network-observe     -
:opengl              -
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              -
:x11                 -

重新打包我们的应用:

liuxg@liuxg:~/snappy/desktop/helloworld$ hello-xiaoguo.createfiletohome 
Hello a nice World!
This example demonstrates the app confinement
This app tries to write to its own user directory
Succeeded! Please find a file created at /home/liuxg/snap/hello-xiaoguo/x1/test.txt
If do not see this, please file a bug

现在,我们再也没有上面显示的那个错误信息了.


7)应用的shell


虽然我们的hello应用非常简单,但是它确实展示了我们很多希望看到的snap应用的一面.在我们的应用中,我们还有一个应用叫做sh.它事件上是一个shell.我们可以通过如下的方式来启动它:

$ hello-xiaoguo.sh

当这个应用被启动后:

liuxg@liuxg:~$ hello-xiaoguo.sh
Launching a shell inside the default app confinement. Navigate to your
app-specific directories with:

  $ cd $SNAP
  $ cd $SNAP_DATA
  $ cd $SNAP_USER_DATA

bash-4.3$ env | grep snap
SNAP_USER_COMMON=/home/liuxg/snap/hello-xiaoguo/common
SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:
SNAP_COMMON=/var/snap/hello-xiaoguo/common
SNAP_USER_DATA=/home/liuxg/snap/hello-xiaoguo/x4
SNAP_DATA=/var/snap/hello-xiaoguo/x4
PATH=/snap/hello-xiaoguo/x4/bin:/snap/hello-xiaoguo/x4/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
HOME=/home/liuxg/snap/hello-xiaoguo/x4
XDG_DATA_DIRS=/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop
SNAP=/snap/hello-xiaoguo/x4
bash-4.3$ 
我们可以通过这个shell来完成我们想要的任务.比如:

bash-4.3$ cd /home/liuxg
bash-4.3$ pwd
/home/liuxg
bash-4.3$ touch hello.text
touch: cannot touch 'hello.text': Permission denied

我们发现,由于snap安全的限制,我们不能向以前的系统那样,可以自由地在任何地方创建一个我们想要的文件了.还有更多的例子,我们留给你们自己来慢慢地体会.

如果大家对snapcraft想了解更多的话,你可以试一下如下的命令:

$ snapcraft tour
Snapcraft tour initialized in ./snapcraft-tour/
Instructions are in the README, or http://snapcraft.io/create/#tour

我们可以在我们的home里的snapcraft-tour目录中找到我们所需要学习的例程.









作者:UbuntuTouch 发表于2016/7/18 11:12:59 原文链接
阅读:881 评论:0 查看评论

Read more