65.9K
CodeProject 正在变化。 阅读更多。
Home

Galaxy Gear 速度计(基于 Tizen)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (25投票s)

2014年10月16日

CPOL

23分钟阅读

viewsIcon

59192

downloadIcon

956

这是一个为三星Galaxy Gear创建测速仪的教程。

 

引言

我的桌上有一小堆可穿戴设备。有搭载Tizen系统的Galaxy Gearm、搭载Android Wear的Galaxy Live、Google Glass、心率监测器,甚至还有一个2004年的微软S.P.O.T.手表。除了微软S.P.O.T.手表,我都能通过自己的程序以某种方式与所有这些设备进行交互。我觉得为它们都制作一些“Hello World”程序会很有趣。测速仪似乎既简单又功能足够;获取速度并以文本形式显示给用户。尽管功能简单,但制作这样的程序确实触及了可穿戴设备的一些不同方面,这些方面比琐碎的更有用。

在本文中,我将使用搭载Tizen系统的Galaxy Gear制作测速仪。我拥有2013年末发布的原始Galaxy Gear。在撰写本文时,第二代Gear已经发布(第二代有三种不同的型号),第三代Galaxy Gear S也已宣布但尚未发布。第一代和第二代设备之间的功能差异很小。大多数差异可能只在于它们是否有摄像头、是否有心率监测器以及是否有红外发射器。一个显著的区别是其中一款型号的屏幕像素尺寸与其他设备不同。

要开始阅读本文,您需要完成以下操作:

  • 按照三星文章中的描述设置Tizen可穿戴设备SDK
  • 已设置Android开发环境
  • 拥有一块Galaxy Gear系列手表

可以通过使用模拟器进行开发,而无需拥有Galaxy Gear,但我不会。如果您拥有原始Galaxy Gear,您需要应用固件更新,将手表的操作系统从Android更改为Tizen。

 

什么是Tizen

Tizen是三星、英特尔等公司开发的开源移动操作系统。它自称是“万物操作系统”(tizen.org),计划支持电视、手机、可穿戴设备以及更多。目前,直接在硬件上运行的实现只有Galaxy Gear(手表)以及即将推出的另一款设备(Galaxy Gear S)。第一款Tizen手机预计将在明年内推出。Tizen支持本地程序和用HTML编写的程序的开发。基于HTML的项目按照W3C打包Web应用程序(Widgets) - 打包和XML配置进行打包。我将在这个第一个程序中使用HTML开发环境。

GPS在哪里

如果您查看Tizen文档,其中提到了GPS。Tizen操作系统确实支持并已为GPS定义了API。但是,操作系统中支持某项功能并不意味着使用该操作系统的设备中存在该硬件(例如:Windows支持计算机拥有GPS,但并非所有计算机都具有GPS接收器)。我正在使用的手表**没有**GPS。为了使此程序正常工作,有必要向手机发送请求以打开其GPS并将信息中继回手表。

在研究如何实现此功能时,我偶然发现了三星远程传感器SDK。它用于从一个设备获取传感器数据并发送到另一个设备。结果这并非我所想。此功能用于从手表检索传感器数据并将其传回手机。我需要数据沿相反方向流动。经过一番研究,我发现我需要制作自己的Android应用程序来为我检索此信息。我不需要Android应用程序有任何UI,因此它只需作为服务运行。更深入地研究后,我发现我需要的是三星附件SDK。

支持GPS的基于Tizen的手表即将推出,但在我撰写本文时尚未发布。我将在本文末尾讨论如何解决这个问题。

三星附件SDK

三星附件SDK是三星提供的一种解决方案,用于设备之间进行交互。设备可以扮演提供者或消费者的角色,并且可以连接到具有互补关系的多个应用程序实例。附件SDK负责处理发现充当提供者的应用程序和服务以及设备上的服务的详细信息,并抽象化它们之间通信的细节。通信可以通过多种传输方式(Wi-Fi、蓝牙、USB和其他一些方式)进行。消费者可以连接到许多提供者,或者提供者可以连接到许多消费者。

参与这些交互的设备通常被称为_附件对等体_。对等体将暴露一个或多个软件实例,这些实例要么提供信息和功能,要么消耗信息和功能。无论它是提供者还是消费者,提供或消耗的软件都称为_附件对等代理_(在Android端的Java代码中由`SAAgent`类表示)。对等代理通过_服务连接_连接(在Java代码中由`SASocket`类定义)。在一个服务连接中可以存在许多_服务通道_。服务通道是连接中的逻辑单元。每个服务通道可以为正在传输的数据分配不同的QoS(服务质量)参数,例如连接是否可靠(未送达的消息是否会重新送达)或不可靠(丢弃的数据包将丢失且不重传),是否需要快速连接,以及可选的建立连接所需的时间。

应用程序的连接信息必须在_服务配置文件_中注册。服务配置文件在XML中定义,并且将XML作为资源包含在您的项目中是确保注册发生所需的全部操作。在此配置文件中,包含唯一标识您的对等代理的信息以及通信通道的QoS参数。

创建视觉界面

该项目的视觉界面将完全在手表上运行。手机除了显示GPS激活的通知外,不会呈现任何界面。在Tizen for Wearables IDE中,创建一个新的jQuery项目。IDE构建的项目将在触摸屏幕时在名为`content_text`的文本元素中切换两个单词。删除此元素中的短语,并将其替换为表示软件正在等待GPS连接的其他内容。应用程序启动时,这将是尚未接收到GPS信息的指示。UI代码如下所示。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <meta name="description" content="A single-page template generated by Tizen Wearable Web IDE"/>

    <title>Speedometer</title>

    <script type="text/javascript" src="js/jquery-1.9.1.js"></script>
    <script type="text/javascript" src="js/main.js"></script>
    <link rel="stylesheet" href="css/style.css" />
</head>
<body>
  <div class=contents>
  	<div style='margin:auto;'>
  		<span class=content_text id=textbox>waiting on gps</span>
  	</div>
  </div>
</body>
</html>
位置通知图标是手机上唯一表明我们的服务正在运行的视觉指示器

注册消费者

如果引用了包含配置文件定义的XML文件,则Tizen项目中的注册会自动发生。实现此目的的两个部分是创建文件并让系统知道该文件位于何处。在Tizen项目的项目根目录下创建一个名为`res`的文件夹。在`res`中创建一个名为`xml`的子文件夹。在`xml`文件夹内创建一个XML文件。我将其命名为`sapservices.xml`。如果您愿意,可以为您的文件选择不同的名称,但请记住在我指定`sapservices.xml`的地方替换您选择的名称。

文档类型定义可以可选地包含在配置文件定义的顶部。我鼓励这样做,因为它可以帮助识别定义中的错误。

<!DOCTYPE resources [
<!ELEMENT resources (application)>
<!ELEMENT application (serviceProfile)+>
<!ATTLIST application name CDATA #REQUIRED>
<!ELEMENT serviceProfile (supportedTransports, serviceChannel+) >
<!ATTLIST application xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceProfile xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceProfile serviceImpl CDATA #REQUIRED>
<!ATTLIST serviceProfile role (PROVIDER | CONSUMER | provider | consumer) #REQUIRED>
<!ATTLIST serviceProfile name CDATA #REQUIRED>
<!ATTLIST serviceProfile id CDATA #REQUIRED>
<!ATTLIST serviceProfile version CDATA #REQUIRED>
<!ATTLIST serviceProfile serviceLimit
(ANY | ONE_ACCESSORY | ONE_PEERAGENT | any | one_peeragent | one_accessory) #IMPLIED>
<!ATTLIST serviceProfile serviceTimeout CDATA #IMPLIED>
<!ELEMENT supportedTransports (transport)+>
<!ATTLIST supportedTransports xmlns:android CDATA #IMPLIED>
<!ELEMENT transport EMPTY>
<!ATTLIST transport xmlns:android CDATA #IMPLIED>
<!ATTLIST transport type (TRANSPORT_WIFI | TRANSPORT_BT | TRANSPORT_BLE | TRANSPORT_USB | transport_wifi | transport_bt | transport_ble | transport_usb) #REQUIRED>
<!ELEMENT serviceChannel EMPTY>
<!ATTLIST serviceChannel xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceChannel id CDATA #REQUIRED>
<!ATTLIST serviceChannel dataRate (LOW | HIGH | low | high) #REQUIRED>
<!ATTLIST serviceChannel priority (LOW | MEDIUM | HIGH | low | medium | high) #REQUIRED>
<!ATTLIST serviceChannel reliability (ENABLE | DISABLE | enable | disable ) #REQUIRED>
]>

文档类型定义之后是注册信息。我们需要指定一些信息。这包括应用程序的名称,后跟一个或多个服务配置文件的信息。一个应用程序(无论是消费者还是提供者)可以包含多个服务配置文件。对于我的应用程序,一个服务配置文件就足够了。服务配置文件需要有一个友好的名称和一个id。对于名称,我使用`speedometer`,对于id,我将使用`/system/speedometer`。分配给手表应用程序的角色是`consumer`,此应用程序的版本号是`1.0`。文档类型定义指定可以定义`serviceTimeout`值。我们不需要在此场景中指定此值(我将稍后解释何时需要此值)。所有这些信息将按如下方式在XML文件中指定。

<resources>
    <application name="SpeedService">
        <serviceProfile
        	name="speedometer"
        	id="/system/speedometer" 
            role="consumer"              
            serviceTimeout="30" 
            serviceLimit="one_peeragent"
            version="1.0" 
            >
        </serviceProfile>
    </application>
</resources>

这个定义还不完整。还需要两个信息;应用程序支持的通信传输以及至少一个`serviceChannel`的定义。一个服务至少需要一个`serviceChannel`进行通信,但可以有多个。对于这个应用程序,只需要一个。服务通道由一个数字ID标识(我任意选择了值`149`)。还需要三个其他属性作为服务质量参数(QoS)。一个`dataRate`,可以是`low`或`high`,一个`priority`,可以是`high`、`medium`或`low`,以及一个`reliability`,可以设置为`enable`或`disable`。当`reliability`设置为`enable`时,如果通信中丢失了消息,它将自动重新发送。这会产生一些额外的开销。将这些元素添加到文件中后,我得到了以下内容。新部分以粗体显示。

<resources>
    <application name="SpeedService">
        <serviceProfile
        	name="speedometer"
        	id="/system/speedometer" 
            role="consumer"              
            serviceTimeout="30" 
            serviceLimit="any"
            version="1.0" 
            >
            <supportedTransports>    
			    <transport type="TRANSPORT_BT" />             
            </supportedTransports>
            <serviceChannel 
               id="149"
               dataRate="low" 
               reliability="enable" 
               priority="low" 
               />
               <serviceChannel 
               id="150"
               dataRate="low" 
               reliability="enable" 
               priority="low" 
               />
        </serviceProfile>
    </application>
</resources>

现在配置文件已经定义,我们需要在配置文件(`config.xml`)中指定其位置。该位置在`tizen:metadata`元素中指定,其中`key`名为`AccessoryServicesLocation`,值设置为配置文件定义的路径(`/res/xml/sapservices.xml`)。当我们修改`config.xml`文件时,需要使用`tizen:privilege`标签添加一个权限。此权限赋予应用程序访问三星附件协议相关功能的权限。`tizen:privilege`标签有一个属性`name`,其值需要为`http://developer.samsung.com/privilege/accessoryprotocol`。

<?xml version="1.0" encoding="UTF-8"?>
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets" id="http://yourdomain/Speedometer" version="1.0.0" viewmodes="maximized">
    <tizen:application id="fWZfEb6GWJ.Speedometer" package="fWZfEb6GWJ" required_version="2.2"/>
    <content src="index.html"/>
    <feature name="http://tizen.org/feature/screen.size.all"/>
    <icon src="icon.png"/>
    <name>Speedometer</name>
    <tizen:privilege name="http://developer.samsung.com/privilege/accessoryprotocol"/>
     <tizen:metadata key="AccessoryServicesLocation" value="res/xml/sapservices.xml"/>
</widget>

编写消费者代码

消费者代码将用JavaScript编写。打开`/js/main.js`。此文件中已有一些代码。在更改它之前,让我们在文件顶部添加一些变量。

手表/消费者将负责启动与手机/提供者的连接。连接可以通过几个步骤完成。手表需要实例化一个附件代理(`SAAgent`)并访问该代理的连接(`SASocket`)。对于连接,我们将需要使用之前在服务配置文件中定义的相同通道ID。我任意选择了ID号149。我需要知道提供者端应用程序使用的名称(注意:提供者代码尚未编写。我将其称为`SpeedService`)。最后,手机获取当前速度(米/秒)。我正在手表端进行转换为其他速度单位。因此,我正在定义一些将用于转换的值。

var SAAgent = null;
var SASocket = null;
var CHANNELID = 149;
var ProviderAppName = "SpeedService";

var MilesPerHour        = {factor:2.23694, 	label:"MPH"};
var KilometersPerHour   = {factor:3.6, 		label:"KPH"};


//default to Kilometer's per hour
var currentUnit = KilometersPerHour;

现有代码会在有人点击屏幕时更改屏幕上显示的文本。将其更改为调用尚未定义的`connect()`方法。

$(window).load(function(){
	document.addEventListener('tizenhwkey', function(e) {
        if(e.keyName == "back")
            tizen.application.getCurrentApplication().exit();
    });
	connect();
});

接下来的几段代码都用于建立连接。它们通过一系列回调方法串联在一起,这些回调方法由其他方法设置和触发。在`connect()`方法中,我们需要请求一个`SAAgent`实例。当我们请求`SAAgent`时,代码中定义的每个`serviceProvider`都将返回一个。由于我们只定义了一个`serviceProvider`,因此将返回一个包含一个元素的数组。`connect`方法将首先使用`webapis.sa.requestSAAgent()`方法请求`SAAgent`。此方法接受两个函数作为参数。第一个函数是在调用成功时调用的函数。第二个函数是在发生错误时调用的函数。在成功函数中,我们将继续连接过程,获取第一个(也是唯一一个)`SAAgent`,并要求它使用`SAAgent.findPeerAgents()`在其他设备上查找其对等代理。在此方法调用之前,需要使用`SAAgent.setPeerAgentFindListener()`在`SAAgent`上设置一个回调对象。

function onsuccess (agents) {
	try {
		if(agents.length>0) {
			SAAgent = agents[0];
			SAAgent.setPeerAgentFindListener(peerAgentFindCallback);
			SAAgent.findPeerAgents();
		} 
	}catch(err)  {
		console.log("onsuccess exception [" + err.name + "] msg[" + err.message + "]");
	}
}

function onerror(error) {
	console.log("ONERROR: err [" + error.name + "] msg [" + error.message + "]");	
}


function connect() {
	try {
		console.log("connect():requesting SAAgent (connect)");
		webapis.sa.requestSAAgent(onsuccess	, onerror)
	} catch(err) {
		console.log("onsuccess exception [" + err.name + "] msg[" + err.message + "]");
	}
}

`peerAgentFindCallback`对象定义了两个方法。当找到`peerAgent`时调用`onpeeragentfound`方法。另一个方法`onerror`在尝试查找`peerAgent`失败时调用。当找到对等代理时,我们通过检查其`appName`是否与我们正在寻找的名称匹配来查看它是否是我们正在寻找的对等代理(我之前在`ProviderAppName`变量中定义了它)。如果它是我正在寻找的对等代理,那么我提供一个回调来接收与它的连接(`agentCallback`)并调用`SAAgent.requestServiceConnection()`来接收回调实例。

var peerAgentFindCallback = {
		onpeeragentfound : function(peerAgent) {
			try {					
				if(peerAgent.appName === ProviderAppName) {
					SAAgent.setServiceConnectionListener(agentCallback);
					SAAgent.requestServiceConnection(peerAgent);
				} else {					
				}
			}
			catch(err) {
				console.log("onpeeragentfound exception: [" + err.name + "] msg [" + err.message + "]");
			}
		},
		onerror: function(error) {
			console.log("peerAgentFindCallback error: err [" + error.name + "] msg [" + error.message + "]" + error);	
		}
};

`agentCallback`没有太多工作要做。它接收一个处于连接状态的`SASocket`。它的引用被保存,并设置一个回调,以便在套接字状态改变时断开连接。然后我提供将接收数据的回调。

var agentCallback = {
	onconnect: function (socket) {
		SASocket = socket;
		SASocket.setSocketStatusListener(function (reason) {
			disconnect();
		});
		SASocket.setDataReceiveListener(onreceive); //start listening
	},
	onerror:onerror1
};

在我们继续之前,我们需要知道将接收什么数据以及它的格式和结构。让我们把手表项目放一边,开始处理Android服务。

构建Android服务

我正在使用基于IntelliJ的Android Studio构建Android服务。当内部类的构造函数必须访问其父类的`class`对象时,IntelliJ会表现出一种奇怪的依赖行为。因此,我的类组织与三星提供的示例代码略有不同,但代码与两种IDE都兼容。创建一个新的Android项目,目标版本为Android 4.3或更高版本。此应用程序不需要任何活动。它将是一个仅服务应用程序。

在添加任何代码之前,我想首先向`AndroidManifest.xml`添加一些权限。这些权限是访问位置数据、使用蓝牙(与手表通信所需)以及访问其他一些SDK相关功能所必需的。

    <!--jij:Communication occurs over Bluetooth. There are some bluetooth permissions that we need -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

    <!--jij:Permissions to access location information -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    <!-- SDK and debugging related permissions -->
    <uses-permission android:name="com.samsung.wmanager.APP" />
    <uses-permission android:name="com.samsung.wmanager.ENABLE_NOTIFICATION" />
    <uses-permission android:name="com.samsung.accessory.permission.ACCESSORY_FRAMEWORK"/>
    <uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
    <uses-permission android:name="android.permission.SET_DEBUG_APP" />

需要添加对两个三星SDK的引用:_三星SDK_和_三星附件SDK_。项目需要定义一个用于连接手表的类和一个用于位置服务的类。连接类将继承自`SASocket`,位置服务将继承自`SAAgent`。向项目添加一个名为`SAPServiceProviderConnection`的新类,并使其继承自`SASocket`。给该类一个名为`connectionID`的整数字段和一个类型为`SpeedService`的字段,名为`mParent`。`SpeedService`类尚未定义。如果IDE现在给出任何警告,请不要担心。

扩展SASocket实现

我效仿三星的做法,为每个连接赋予唯一的数字ID值。但我使用它的方式不同。在三星的示例中,ID用于唯一标识正在添加或从连接列表中删除的连接。由于对象是通过引用传递的,因此可以在不给它数字ID的情况下对其进行跟踪;仅对象引用就足够了。我的代码中的`connectionID`仅用于调试目的。

这个扩展的`SASocket`类不需要做太多。当它关闭时,它将请求从父对象的活动连接集合中移除。对于这个项目,数据只需从手机流向手表。因此,`SAPServiceProviderConnection`类在`onReceive()`中忽略所有传入数据。完整的类如下。

package net.j2i.speedometer;

import android.util.Log;
import com.samsung.android.sdk.accessory.SASocket;
import java.io.IOException;


public class SAPServiceProviderConnection extends SASocket {
    private int connectionID;
    static int nextID = 1;
    public final static String TAG = "SAPServiceProvider";
    private SpeedService mParent;

    public void setParent(SpeedService speedService) {
        mParent = speedService;
    }

    public  SAPServiceProviderConnection() {
        super(SAPServiceProviderConnection.class.getName());
        connectionID = ++nextID;
    }

    @Override
    protected void onServiceConnectionLost(int reason) {
        if(mParent!=null) {
            mParent.removeConnection(this);;
        }
    }

    @Override
    public void onReceive(int channelID, byte[] data) {
    }

    @Override
    public void onError(int channelID, String errorString, int errorCode) {
        Log.e(TAG,"ERROR:"+errorString+ " | " + errorCode);
    }
}

实现提供者服务

创建一个名为`SpeedService`的新类。该类需要继承自`SAAgent`。`SAAgent`类继承自`Service`。该类将包含Android程序的入口点,并负责获取和广播速度信息。该类中有几个成员变量。有一个`Binder`对象(服务中常见),一个用于连接集合的成员,以及要使用的服务通道ID。服务中有一个`LocationListener`实例,它将位置信息编码为JSON字符串以转发到手机。正在通信的信息中还有额外的信息。速度、方向和位置与GPS监听器的状态一起传输。相同的服务可用于制作高度计、指南针或手表的其他应用程序。

    SpeedBinder mBinder = new SpeedBinder();
    public final static String TAG = "SpeedService";
    public final static int SAP_SERVICE_CHANNEL_ID = 149;
    AbstractCollection<SAPServiceProviderConnection> mConnectionBag = new Vector<SAPServiceProviderConnection>();
    LocationManager mLocationManager;
    boolean mIsListening = false;


    public class SpeedBinder extends Binder {
        SpeedService getService() {
            return SpeedService.this;
        }
    }

    LocationListener locationListener = new LocationListener() {

    long mTime;
    float   mBearing, mSpeed;
    double mLatitude, mLongitude, mAltitude;
    boolean bHasAltitude, bHasBearing, bHasSpeed;
    boolean bGpsEnabled = true;

    @Override
    public  String toString() {
        //In general this isn't how I would encode something in JSON, but the amount
        //of data is small enough such that I've decided to use String.Format to
        //produce what's needed.
        final String returnValue =
                String.format("{ \"gpsEnabled\" :%b,"+
                                "\"hasSpeed\":%b, \"speed\":%1.2f, \"hasBearing\":%b, \"bearing\":%1.4f,"+
                                "\"latitude\":%f, \"longitude\":%f,\"hasAltitude\":%b, \"altitude\":%1.3f}",
                        bGpsEnabled,
                        bHasSpeed, mSpeed, bHasBearing, mBearing,
                        mLatitude, mLongitude, bHasAltitude, mAltitude
                );
        return returnValue;
    }

    @Override
    public void onLocationChanged(Location location) {
        //not that the if conditions are also assignment operations
        if(bHasSpeed = location.hasSpeed())
            mSpeed = location.getSpeed();
        if(bHasAltitude = location.hasAltitude())
            mAltitude = location.getAltitude();
        if(location.hasSpeed())
            mSpeed = location.getSpeed();
        if(bHasBearing = location.hasBearing())
            mBearing = location.getBearing();
        mLatitude = location.getLatitude();
        mLongitude = location.getLongitude();
        mTime = location.getTime();
        transmitLocation();
    }

    @Override
    public void onStatusChanged(String s, int i, Bundle bundle) {   }

    @Override
    public void onProviderEnabled(String s) { bGpsEnabled = true; }

    @Override
    public void onProviderDisabled(String s) { bGpsEnabled = false; }
};

`transmitLocation()`方法将当前位置的JSON从`String`转换为`byte`数组,并将其发送到服务拥有的每个活动连接。

public void transmitLocation() {
    String locationString = locationListener.toString();
    byte[] locationMessage = locationString.getBytes();

    Log.i(TAG, locationString);

    Iterator connectionIterator = mConnectionBag.iterator();
    while(connectionIterator.hasNext()) {
        SAPServiceProviderConnection connection = (SAPServiceProviderConnection)connectionIterator.next();
        try {
            connection.send(SAP_SERVICE_CHANNEL_ID, locationMessage);
        } catch(IOException exc) {
            //
        }
    }
}

当连接建立或关闭时,它们会被添加到或从连接集合中移除。当`mConnectionBag`成员发生变化时,该类会评估GPS是否需要保持活跃。当没有活动的监听器时,没有理由保持GPS硬件活跃;这样做会消耗电池,并且如果发现手机在用户与位置无关时监控其位置,可能会引起担忧。`reevaluateLocationManager()`会在连接集合中添加新连接时调用`startTracking()`。在跟踪已启用时调用此方法不会产生负面影响;如果跟踪处于活跃状态,该方法不会执行任何操作。一旦连接计数达到零,就会调用`stopTracking()`。

public void removeConnection(SAPServiceProviderConnection connection) {
    mConnectionBag.remove(connection);
    reevaluateLocationManager();
}

public void addConnection(SAPServiceProviderConnection connection) {
    mConnectionBag.add(connection);
    transmitLocation();
}

void startTracking() {
    if(!mIsListening) {
        mIsListening = true;
        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, locationListener);
    }
}

void stopTracking() {
    if(mIsListening) {
        mLocationManager.removeUpdates(locationListener);
        mIsListening = false;
    }
}

void reevaluateLocationManager() {
    if(mConnectionBag.size()==0)
        stopTracking();
    else
        startTracking();
}

该服务应运行直到明确停止。服务的`onStart`方法返回`START_STICKY`以告知Android系统这一点。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Toast.makeText(getBaseContext(), "started", Toast.LENGTH_LONG).show();
    Log.i(TAG, "started");
    return START_STICKY;
}

配置提供者代理配置文件

提供者代理配置文件看起来与为消费者创建的配置文件几乎相同。与Tizen端一样,我将配置文件放在`/re/xml`中。XML文件的名称将是`sapservices.xml`。此文件与Tizen项目中的文件内容不同之处在于,它被分配了`provider`角色而不是`consumer`角色,并且存在一个名为`serviceImpl`的新属性,指向包含代理实现的Java类。此配置文件还具有`any`的服务限制,允许它服务于可能连接到它的任意数量的附件对等体。消费者的限制设置为`one`;它将只从单个设备获取速度信息。

<resources>
    <application name="SpeedService">
        <serviceProfile
            name="speedometer"
            id="/system/speedometer"
            role="provider"
            serviceLimit="any"
            version="1.0"
            serviceImpl="net.j2i.speedometer.SpeedService"
            >
            <supportedTransports>
                <transport type="TRANSPORT_BT"/>
            </supportedTransports>
            <serviceChannel
                id="149"
                dataRate="low"
                reliability="enable"
                priority="low"
                 />
        </serviceProfile>
    </application>
</resources>

最后需要进行的更改是,`AndroidManifest.xml`需要有一个条目,指示服务配置文件的定义位置,并且需要声明服务以及它响应的意图。

<service android:name=".SpeedService"  />

<receiver android:name = "com.samsung.android.sdk.accessory.ServiceConnectionIndicationBroadcastReceiver">
    <intent-filter>
        <action android:name="android.accessory.service.action.ACCESSORY_SERVICE_CONNECTION_IND"/>
    </intent-filter>
</receiver>
<receiver android:name = "com.samsung.android.sdk.accessory.RegisterUponInstallReceiver">
    <intent-filter>
        <action android:name="android.accessory.device.action.REGISTER_AFTER_INSTALL"/>
    </intent-filter>
</receiver>

<meta-data android:name="AccessoryServicesLocation" android:value="/res/xml/sapservices.xml" />

更新手表的显示

此时,手表项目能够建立连接并从提供者接收数据,但并未对接收到的内容执行任何操作。创建一个`onReceive`函数。它接受的参数将是通道ID的整数和一个保存接收到的数据的字符串。当接收到数据时,我检查以确保接收到的数据是在我预期的`CHANNELID`上。这并非严格必要,因为数据不能在未定义的通道上进入,而我只定义了一个。但我这样做是为了以后我决定更改程序以允许传输其他数据。当位置数据进入时,我取`speed`组件,将其通过格式化/转换函数,并将文本框的内容设置为格式化的速度。

function onreceive(channelid, data) {	
	console.log("received:"+data);
	switch(channelid) {
	case CHANNELID: {
		try {
			var locationData = jQuery.parseJSON(data);
			$('#textbox').html( formatSpeed(locationData.speed));
		}
		catch(e) {
			$('#textbox').html("failed to parse");
		}
	}
		break;
		default:
			break;
	}	
}

将手表应用程序和Android应用程序一起打包

应用程序的两个部分可以一起打包并部署到手机。如果将编译好的手表应用程序复制到Android项目的路径`/res/assets`,Gear Manager将负责发现手表应用程序并将其复制到手表。启动调试器,以便项目部署到手机。由于应用程序中没有声明任何活动,您可能会在部署后收到提示,询问下一步该怎么做。无需启动任何活动,因此允许应用程序部署。部署片刻后,服务和手表应用程序的发现将发生。应用程序将部署到手表,服务将启动。

请注意,如果您更新手表应用程序,您需要将更新后的应用程序复制到服务项目的`/res/assets`文件夹。如果您不这样做,那么服务的任何重新部署都将导致手表拥有该文件夹中存在的旧版本手表应用程序。

手表上的速度读数。

添加设置屏幕

手表应用程序设置为以公里/小时显示速度。显示其他单位速度所需的信息已经存在,但无法选择使用哪种单位转换。在用户界面中,我希望有一个不同的页面来更改此设置。让我们回顾一下手表应用程序``标签的内容。

 <div class=contents>
 	<div style='margin:auto;'>
 		<span class=content_text id=textbox>waiting on gps</span>
 	</div>
 </div>

预定义了用于导航的 CSS 类。主要的感兴趣的 CSS 类是 `ui-page` 和 `ui-page-active`。我的应用程序中的每个_页面_或视图都将是同一个 HTML 文件的一部分,但由具有 `ui-page` 类的 `

` 元素定义。我将默认激活的页面标记为 `ui-page` 和 `ui-page-active`,并且应用程序内的导航将通过更改分配给每个_页面_的类来实现。

最初,大部分导航看起来像基于常规锚点标签的导航。`href`属性的值将是它导航到的`

`的`id`。这是一个带有两个div元素之间导航的HTML内容的简单版本。

<body>
   <div class="ui-page ui-page-active" id="main">
      <div class="ui-header" data-position="fixed"><h2>Page One</h2></div>
      <div class="ui-content">
         <a href="#secondPage">to second page</a>
      </div>
   </div>
   <div class="ui-page" id="secondPage">
      <div class="ui-header" data-position="fixed"><h2>Page One</h2></div>
      <div class="ui-content">
      </div>
   </div>
</body>

这种导航形式的部分机制是在 Tizen Advanced UI 框架 (TAU) 中实现的。为此,您需要在项目中包含 TAU 代码并在 HTML 中引用它。该框架通常位于路径 `lib/tau/wearable/js`。如果您的项目中不存在此路径,您可以使用“Basic”模板创建第二个项目并从那里复制文件。请记住将 TAU 的 CSS 和 JavaScript 引用放在页面的头部

    <link rel="stylesheet"  href="lib/tau/wearable/theme/default/tau.min.css">
    <script type="text/javascript" src="lib/tau/wearable/js/tau.js"></script>

设置屏幕将存在于同一个HTML文件中。目前我只希望允许用户在SI和英制速度单位之间切换。速度单位已经在页面的JavaScript中定义。我将以编程方式构建部分界面,而不是重复定义它。

function populateUnits() {
	var unitList = document.getElementById('unitsList');
	UnitList.forEach(function(e) {
		var li = document.createElement('li');
		var radio = document.createElement('input');
		var label = document.createElement('label');
				
		radio.setAttribute('type','radio' );
		radio.setAttribute('name', 'units');
		radio.setAttribute('value', e.label);
		radio.setAttribute('id', 'unit_' + e.label);
		if(e.label == currentUnit.label)
			radio.setAttribute('checked','true');
		radio.setAttribute('onclick', 'setUnit("'+ e.label + '");');
		
		label.innerText = e.label;
		label.setAttribute('for', 'unit_', e.label );
		label.innerText = e.label
		
		li.appendChild(radio);
		li.appendChild(label);		
		radio.setAttribute('href','#');
		unitList.appendChild(li);		
	});		
}
最终的速度单位选择屏幕

显示可用单位的单选按钮。当选中一个单选按钮时,会调用一个名为`setUnit`的函数,该函数将保存选择并更改用于显示速度的转换单位。

function setUnit(unit ) {
	localStorage.setItem("speedUnit", unit);
	applySpeedSetting();
}

function applySpeedSetting() {
	var unit = localStorage.getItem('speedUnit');
	if(unit!=null) {
		UnitList.forEach( function(x) {
			if(x.label == unit) {
				currentUnit = x;
				return;
			}			
		});
	}	
}

W3C Web Storage API描述了`localStorage`对象,可用于存储键/值对。`applySettings`函数在选定的速度单位发生变化时调用,并且在程序生命周期开始时也会调用,以应用保存的选择。它会遍历代码中定义的速度单位,直到找到其标签与保存的值匹配的单位。找到匹配项后,它将此对象设置为要使用的当前单位并停止搜索。

与Gear S的兼容性

我最初是在 Galaxy Gear S 发布之前编写的。Gear S 除了其他功能外,还拥有自己的 GPS 无线电;它应该能够在不与手机通信的情况下检测速度。我希望这个程序能够利用即将推出的手机功能。对于基于 HTML 的项目,位置信息通过 W3C 地理定位 API 规范公开。

在Tizen中,位置信息需要特权。由于此应用程序可以使用来自手机的位置信息,而来自手表的位置信息不是强制性的,因此我在程序的清单中将其列为特权而不是要求。以下行已添加到`config.xml`中以指定此特权。

<tizen:privilege name="http://tizen.org/privilege/location"/>

要使用地理定位API,首先检查`navigator`对象上是否存在`geolocation`字段。如果未找到该字段,则无法使用此方法,并回退到三星附件SDK和Android服务。如果找到该对象,尝试使用它通过调用`navigator.geolocation.watchPosition()`请求位置更新。`watchPosition`方法接收一个回调函数,位置信息将发送到该函数;一个在发生错误时调用的方法;以及一个指示接收到的位置信息最大年龄的参数。当接收到位置信息时,它与来自手机的信息以相同的方式处理。如果在尝试使用手表的GPS时出现任何错误,程序将回退到手机。

这些更改通过进行以下代码更改来实现

function locationCallbackSuccess(position ) {
	if(position.speed)
		$('#textbox').html( formatSpeed(position.speed));
}

function locationCallbackFailed( reason ) {
	//connect to the phone and rely on it's GPS
	connect();
}

$(window).load(function(){
	document.addEventListener('tizenhwkey', function(e) {
        if(e.keyName == "back") {
        	var currentPage = document.getElementsByClassName('ui-page-active')[0];
        	var pageId = (currentPage)?currentPage.id:' ';
        	if(pageId=='main')
        		tizen.application.getCurrentApplication().exit();
        	else {
        		tau.changePage("#main");
        	}       		
        }
    });
	tau.defaults.pageTransition = "slideup";
	
	if(navigator.geolocation) {
		navigator.geolocation.watchPosition(
           locationCallbackSuccess, 
           locationCallbackFailed, 
           {maximumAge:250}
        );
	}
	else
		connect();
	populateUnits();
	applySpeedSetting();
});
	

性能和电池

在获得Galaxy Gear S后,我尝试将测速仪作为独立应用程序使用。结果与我预期的不完全一样;Gear S似乎以低于我期望的频率更新位置和速度信息。即使我要求每半秒更新一次信息,我也只能每2到3秒收到一次位置信息。尽管Gear S确实有自己的GPS硬件,但我建议的解决方案是首先尝试使用手机的硬件获取位置信息,并且只有在手机无法用于位置信息时(例如:手表与手机断开连接或手机上禁用了位置服务)才回退到Gear的硬件。除了获得更好的更新频率外,对Gear S电池的消耗也会降低。 

有改进空间

请记住,这里提供的代码旨在超越“Hello World”程序,因此仍有很大的改进空间。如果您想完善此代码以使其适合应用商店,您可能需要考虑做一些事情。如果位置信息停止发送到手表,则没有丢失信息的指示。指示位置信息丢失将有所帮助,并减少用户认为丢失位置信息是由于程序故障造成的可能性。我以纯文本形式显示了速度信息。也可以以熟悉的图形方式呈现。用户可能还希望在程序运行时手表屏幕保持开启,而不是超时。最后,自己尝试使用该程序,看看您发现了哪些改进机会(但如果您自己驾驶车辆,请不要使用它;只将其作为乘客使用!)。

获取帮助

如果您有问题或需要帮助,请随时在下方提问。我会尽快回复。您也可以在Tizen开发者论坛三星开发者论坛提问。

历史

  • 2014年10月16日 - 初次发布
  • 2014年12月3日 - 添加Gear S推荐
© . All rights reserved.