community-cordova-plugin-nfc 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ /* globals: export, module, console, document, ndef, Windows, Uint8Array */
2
+
3
+ "use strict";
4
+
5
+ var STATUS_NFC_OK = "NFC_OK";
6
+ var STATUS_NO_NFC = "NO_NFC";
7
+ var STATUS_NFC_DISABLED = "NFC_DISABLED";
8
+ // I don't think there is a Windows API to determine if NFC exists
9
+ var STATUS_NO_NFC_OR_NFC_DISABLED = "NO_NFC_OR_NFC_DISABLED";
10
+ var STATUS_NDEF_PUSH_DISABLED = "NDEF_PUSH_DISABLED";
11
+
12
+ var ndefUtils = {
13
+ // convert Uint8Array to []
14
+ toArray: function (bytes) {
15
+ var output = [], i = 0;
16
+ for (; i < bytes.length; i += 1) {
17
+ output.push(bytes[i]);
18
+ }
19
+
20
+ return output;
21
+ }
22
+ };
23
+
24
+ var self = {
25
+ subscribedMessageId: -1,
26
+ publishedMessageId: -1,
27
+ proximityDeviceStatus: STATUS_NO_NFC_OR_NFC_DISABLED,
28
+ listeningForNonNdefTags: false,
29
+ tagEventTimeoutId: -1,
30
+ initializeProximityDevice: function() {
31
+ if (self.proximityDevice) {
32
+ // TODO Is there an API to tell if the user disabled NFC?
33
+ try {
34
+ // KLUDGE this call fails when the user has disabled the device
35
+ var kludge = self.proximityDevice.maxMessageBytes;
36
+ self.proximityDeviceStatus = STATUS_NFC_OK;
37
+ } catch (e) {
38
+ console.log(e);
39
+ self.proximityDeviceStatus = STATUS_NFC_DISABLED;
40
+ }
41
+ return self.proximityDeviceStatus === STATUS_NFC_OK;
42
+ }
43
+
44
+ // try to get device again because user might have re-enabled the device
45
+ self.proximityDevice = Windows.Networking.Proximity.ProximityDevice.getDefault();
46
+
47
+ // TODO use these events to implement nfc.addTagDiscoveredListener
48
+ if (self.proximityDevice) {
49
+ self.proximityDevice.ondevicearrived = function (eventArgs) {
50
+ console.log("NFC tag detected");
51
+ if (self.listeningForNonNdefTags) {
52
+ // set a timeout so NDEF tags can cancel this event
53
+ // we want one event to mimic the Android behavior
54
+ self.tagEventTimeoutId = setTimeout(self.fireTagEvent, 100);
55
+ }
56
+ };
57
+
58
+ self.proximityDevice.ondevicedeparted = function (eventArgs) {
59
+ console.log("NFC tag is gone");
60
+ };
61
+ } else {
62
+ self.proximityDeviceStatus = STATUS_NO_NFC_OR_NFC_DISABLED;
63
+ }
64
+
65
+ return self.proximityDeviceStatus === STATUS_NFC_OK;
66
+ },
67
+ init: function (success, failure, args) {
68
+
69
+ self.initializeProximityDevice();
70
+
71
+ if (!self.proximityDevice) {
72
+ console.log("WARNING: proximity device is null");
73
+ }
74
+
75
+ success();
76
+ },
77
+ registerNdef: function (success, failure, args) {
78
+ console.log("Listening for NFC tags with NDEF messages.");
79
+
80
+ if (!self.initializeProximityDevice()) {
81
+ failure(self.proximityDeviceStatus);
82
+ return;
83
+ }
84
+
85
+ try {
86
+ self.subscribedMessageId = self.proximityDevice.subscribeForMessage("NDEF", self.messageReceivedHandler);
87
+ success();
88
+ } catch (e) {
89
+ console.log(e);
90
+ failure(e);
91
+ }
92
+ },
93
+ removeNdef: function (success, failure, args) {
94
+
95
+ console.log("Removing NDEF Listener");
96
+
97
+ if (!self.initializeProximityDevice()) {
98
+ failure(self.proximityDeviceStatus);
99
+ return;
100
+ }
101
+
102
+ try {
103
+ if (self.subscribedMessageId !== -1) {
104
+ self.proximityDevice.stopSubscribingForMessage(self.subscribedMessageId);
105
+ self.subscribedMessageId = -1;
106
+ }
107
+
108
+ success();
109
+ } catch (e) {
110
+ console.log(e);
111
+ failure(e.message);
112
+ }
113
+ },
114
+ addTagDiscoveredListener: function(success, failure, args) {
115
+ self.listeningForNonNdefTags = true;
116
+ success();
117
+ },
118
+ remoteTagDiscoveredListener: function(success, failure, args) {
119
+ self.listeningForNonNdefTags = false;
120
+ success();
121
+ },
122
+ writeTag: function (success, failure, args) {
123
+
124
+ console.log("Write Tag");
125
+
126
+ if (!self.initializeProximityDevice()) {
127
+ failure(self.proximityDeviceStatus);
128
+ return;
129
+ }
130
+
131
+ try {
132
+ var records = args[0];
133
+ var bytes = ndef.encodeMessage(records);
134
+
135
+ self.stopPublishing();
136
+
137
+ var dataWriter = new Windows.Storage.Streams.DataWriter();
138
+ dataWriter.unicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.utf16LE;
139
+ dataWriter.writeBytes(bytes);
140
+
141
+ self.publishedMessageId = self.proximityDevice.publishBinaryMessage("NDEF:WriteTag",
142
+ dataWriter.detachBuffer(),
143
+ function (sender, messageId) {
144
+ console.log("Successfully wrote message to the NFC tag.");
145
+ self.stopPublishing();
146
+
147
+ success();
148
+ }
149
+ );
150
+
151
+ } catch (e) {
152
+ console.log(e);
153
+ failure(e.message);
154
+ }
155
+ },
156
+ shareTag: function(success, failure, args) {
157
+
158
+ console.log("Share Tag");
159
+
160
+ if (!self.initializeProximityDevice()) {
161
+ failure(self.proximityDeviceStatus);
162
+ return;
163
+ }
164
+
165
+ try {
166
+ var records = args[0];
167
+ var bytes = ndef.encodeMessage(records);
168
+
169
+ self.stopPublishing();
170
+
171
+ var dataWriter = new Windows.Storage.Streams.DataWriter();
172
+ dataWriter.unicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.utf16LE;
173
+ dataWriter.writeBytes(bytes);
174
+
175
+ self.publishedMessageId = self.proximityDevice.publishBinaryMessage("NDEF",
176
+ dataWriter.detachBuffer(),
177
+ function (sender, messageId) {
178
+ console.log("Successfully shared message over peer-to-peer.");
179
+ self.stopPublishing();
180
+
181
+ success();
182
+ });
183
+
184
+ } catch (e) {
185
+ console.log(e);
186
+ failure(e.message);
187
+ }
188
+ },
189
+ unshareTag: function(success, failure, args) {
190
+ console.log("Unshare Tag");
191
+
192
+ if (!self.initializeProximityDevice()) {
193
+ failure(self.proximityDeviceStatus);
194
+ return;
195
+ }
196
+
197
+ try {
198
+ self.stopPublishing();
199
+ success();
200
+ } catch (e) {
201
+ console.log(e);
202
+ failure(e.message);
203
+ }
204
+ },
205
+ enabled: function(success, failure, args) {
206
+ self.initializeProximityDevice();
207
+
208
+ if (self.initializeProximityDevice()) {
209
+ success();
210
+ } else {
211
+ failure(self.proximityDeviceStatus);
212
+ }
213
+
214
+ },
215
+ showSettings: function(success, failure, args) {
216
+
217
+ // WARNING: this isn't documented, so it might break
218
+ var nfcSettingsUri = "ms-settings-proximity:";
219
+ var uri = new Windows.Foundation.Uri(nfcSettingsUri);
220
+
221
+ Windows.System.Launcher.launchUriAsync(uri).then(
222
+ function (settingsAppeared) {
223
+ if (settingsAppeared) {
224
+ success();
225
+ } else {
226
+ failure();
227
+ }
228
+ }
229
+ );
230
+ },
231
+ stopPublishing: function() {
232
+ if (self.publishedMessageId !== -1) {
233
+ self.proximityDevice.stopPublishingMessage(self.publishedMessageId);
234
+ self.publishedMessageId = -1;
235
+ }
236
+ },
237
+ messageReceivedHandler: function (sender, message) {
238
+
239
+ // this is an NDEF message so cancel the tag event before it fires
240
+ clearTimeout(self.tagEventTimeoutId);
241
+
242
+ var bytes = new Uint8Array(message.data.length);
243
+ var dataReader = Windows.Storage.Streams.DataReader.fromBuffer(message.data);
244
+ dataReader.readBytes(bytes);
245
+ dataReader.close();
246
+
247
+ var byteArray = ndefUtils.toArray(bytes);
248
+ var ndefMessage = ndef.decodeMessage(byteArray);
249
+ console.log(JSON.stringify(ndefMessage));
250
+
251
+ // on windows, tag only contains the ndef message
252
+ // other platforms have tag data
253
+ var tag = {
254
+ ndefMessage: ndefMessage
255
+ };
256
+
257
+ // fire JavaScript event with NDEF data
258
+ var e = document.createEvent('Events');
259
+ e.initEvent("ndef", true, false);
260
+ e.tag = tag;
261
+ document.dispatchEvent(e);
262
+ },
263
+ fireTagEvent: function() {
264
+ var e = document.createEvent('Events');
265
+ e.initEvent("tag", true, false);
266
+ // unfortunately we don't have any tag metadata
267
+ e.tag = {};
268
+ document.dispatchEvent(e);
269
+ }
270
+ };
271
+
272
+ module.exports = {
273
+ init: self.init,
274
+ registerNdef: self.registerNdef,
275
+ removeNdef: self.removeNdef,
276
+ registerTag: self.addTagDiscoveredListener,
277
+ remoteTag: self.removeTagDiscoveredListener,
278
+ writeTag: self.writeTag,
279
+ shareTag: self.shareTag,
280
+ unshareTag: self.unshareTag,
281
+ enabled: self.enabled,
282
+ showSettings: self.showSettings
283
+ }
284
+
285
+ require("cordova/exec/proxy").add("NfcPlugin", module.exports);
@@ -0,0 +1,191 @@
1
+ using System.Runtime.Serialization;
2
+
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.Linq;
6
+ using System.Text;
7
+ using System.Threading.Tasks;
8
+ using System.IO;
9
+
10
+ using System.Runtime.InteropServices.WindowsRuntime;
11
+ using Windows.Storage.Streams;
12
+
13
+ namespace ChariotSolutions.NFC.NDEF
14
+ {
15
+ public class Ndef
16
+ {
17
+ public static List<NdefRecord> parse(byte[] bytes)
18
+ {
19
+ List<NdefRecord> records = new List<NdefRecord>();
20
+ int index = 0;
21
+
22
+ while (index <= bytes.Length)
23
+ {
24
+ byte tnf_byte = bytes[index];
25
+ bool mb = (tnf_byte & 0x80) != 0;
26
+ bool me = (tnf_byte & 0x40) != 0;
27
+ bool cf = (tnf_byte & 0x20) != 0;
28
+ bool sr = (tnf_byte & 0x10) != 0;
29
+ bool il = (tnf_byte & 0x8) != 0;
30
+ int tnf = tnf_byte & 0x7;
31
+
32
+ if (cf)
33
+ {
34
+ // TODO implement me
35
+ throw new IOException("Chunked records are not supported.");
36
+ }
37
+
38
+ index++;
39
+ int typeLength = bytes[index];
40
+ int idLength = 0;
41
+ int payloadLength = 0;
42
+
43
+ if (sr)
44
+ {
45
+ index++;
46
+ payloadLength = bytes[index];
47
+ }
48
+ else
49
+ {
50
+ payloadLength = ((0xFF & bytes[++index]) << 24) |
51
+ ((0xFF & bytes[++index]) << 26) |
52
+ ((0xFF & bytes[++index]) << 8) |
53
+ (0xFF & bytes[++index]);
54
+ }
55
+
56
+ if (il)
57
+ {
58
+ index++;
59
+ idLength = bytes[index];
60
+ }
61
+
62
+ index++;
63
+ IBuffer type = bytes.AsBuffer(index, typeLength);
64
+ index += typeLength;
65
+
66
+ IBuffer id = bytes.AsBuffer(index, idLength);
67
+ index += idLength;
68
+
69
+ IBuffer payload = bytes.AsBuffer(index, payloadLength);
70
+ index += payloadLength;
71
+
72
+ NdefRecord record = new NdefRecord();
73
+ record.tnf = (byte)tnf;
74
+ record.type = typeLength > 0 ? type.ToArray() : new byte[0];
75
+ record.id = idLength > 0 ? id.ToArray() : new byte[0];
76
+ record.payload = payloadLength > 0 ? payload.ToArray() : new byte[0];
77
+
78
+ records.Add(record);
79
+
80
+ if (me) break; // last message
81
+ }
82
+
83
+ return records;
84
+ }
85
+
86
+ public static byte[] toBytes(NdefRecord[] records)
87
+ {
88
+ MemoryStream encoded = new MemoryStream();
89
+
90
+ for (int i = 0; i < records.Length; i++)
91
+ {
92
+
93
+ bool mb = (i == 0);
94
+ bool me = (i == (records.Length - 1));
95
+ bool cf = false; // TODO
96
+ bool sr = (records[i].payload.Length < 0xFF);
97
+ bool il = (records[i].id.Length > 0);
98
+
99
+ byte tnf_byte = encodeTnf(mb, me, cf, sr, il, records[i].tnf);
100
+ encoded.WriteByte(tnf_byte);
101
+
102
+ int type_length = records[i].type.Length;
103
+ encoded.WriteByte((byte)type_length);
104
+
105
+ int payload_length = records[i].payload.Length;
106
+ if (sr)
107
+ {
108
+ encoded.WriteByte((byte)payload_length);
109
+ }
110
+ else
111
+ {
112
+ // 4 bytes
113
+ encoded.WriteByte((byte)(payload_length >> 24));
114
+ encoded.WriteByte((byte)(payload_length >> 16));
115
+ encoded.WriteByte((byte)(payload_length >> 8));
116
+ encoded.WriteByte((byte)(payload_length & 0xFF));
117
+ }
118
+
119
+ int id_length = 0;
120
+ if (il)
121
+ {
122
+ id_length = records[i].id.Length;
123
+ encoded.WriteByte((byte)id_length);
124
+ }
125
+
126
+ encoded.Write(records[i].type, 0, type_length);
127
+ if (il)
128
+ {
129
+ encoded.Write(records[i].id, 0, id_length);
130
+ }
131
+
132
+ encoded.Write(records[i].payload, 0, payload_length);
133
+ }
134
+ return encoded.ToArray();
135
+ }
136
+
137
+ static byte encodeTnf(bool mb, bool me, bool cf, bool sr, bool il, byte tnf)
138
+ {
139
+ byte value = tnf;
140
+
141
+ if (mb)
142
+ {
143
+ value = (byte)(value | 0x80);
144
+ }
145
+
146
+ if (me)
147
+ {
148
+ value = (byte)(value | 0x40);
149
+ }
150
+
151
+ if (cf)
152
+ {
153
+ value = (byte)(value | 0x20);
154
+ }
155
+
156
+ if (sr)
157
+ {
158
+ value = (byte)(value | 0x10);
159
+ }
160
+
161
+ if (il)
162
+ {
163
+ value = (byte)(value | 0x8);
164
+ }
165
+
166
+ if (cf) // check
167
+ {
168
+ if (!(tnf == 0x06 && !mb && !me && !il))
169
+ {
170
+ throw new IOException("When cf is true, mb, me and il must be false and tnf must be 0x6");
171
+ }
172
+ }
173
+
174
+ return value;
175
+ }
176
+ }
177
+
178
+ [DataContract]
179
+ public class NdefRecord
180
+ {
181
+ [DataMember]
182
+ public byte tnf { get; set; }
183
+ [DataMember]
184
+ public byte[] type { get; set; }
185
+ [DataMember]
186
+ public byte[] id { get; set; }
187
+ [DataMember]
188
+ public byte[] payload { get; set; }
189
+ }
190
+
191
+ }
@@ -0,0 +1,200 @@
1
+ using System.Runtime.Serialization;
2
+ using Windows.Networking.Proximity;
3
+ using WPCordovaClassLib.Cordova;
4
+ using WPCordovaClassLib.Cordova.Commands;
5
+ using WPCordovaClassLib.Cordova.JSON;
6
+
7
+ using System.Runtime.InteropServices.WindowsRuntime;
8
+ using Windows.Storage.Streams;
9
+ using System.Diagnostics;
10
+ using System.IO;
11
+
12
+ using System.Collections.Generic;
13
+ using System.Text;
14
+ using System.Collections;
15
+
16
+ using ChariotSolutions.NFC.NDEF;
17
+
18
+ //
19
+ // http://www.nfc-forum.org/specs/spec_list/
20
+ // http://msdn.microsoft.com/en-us/library/windows/apps/br241250.aspx
21
+ //
22
+
23
+ namespace Cordova.Extension.Commands
24
+ {
25
+ class NfcPlugin : BaseCommand
26
+ {
27
+ private ProximityDevice proximityDevice;
28
+ private long subscribedMessageId = -1;
29
+ private long publishedMessageId = -1;
30
+
31
+ public NfcPlugin()
32
+ {
33
+ Debug.WriteLine("Nfc Plugin");
34
+ proximityDevice = ProximityDevice.GetDefault();
35
+ if (proximityDevice == null) // shouldn't happen
36
+ {
37
+ Debug.WriteLine("WARNING: proximity device is null");
38
+ }
39
+ }
40
+
41
+ public void init(string args)
42
+ {
43
+ // not used for WP8
44
+ }
45
+
46
+ // no args
47
+ public void registerNdef(string args)
48
+ {
49
+ Debug.WriteLine("Registering for NDEF");
50
+
51
+ try
52
+ {
53
+ subscribedMessageId = proximityDevice.SubscribeForMessage("NDEF", MessageReceivedHandler);
54
+ DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
55
+ }
56
+ catch (System.Exception e)
57
+ {
58
+ Debug.WriteLine(e);
59
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message));
60
+ }
61
+ }
62
+
63
+ // no args
64
+ public void removeNdef(string args)
65
+ {
66
+ Debug.WriteLine("Removing NDEF");
67
+
68
+ try
69
+ {
70
+ if (subscribedMessageId != -1)
71
+ {
72
+ proximityDevice.StopSubscribingForMessage(subscribedMessageId);
73
+ subscribedMessageId = -1;
74
+ }
75
+ DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
76
+ }
77
+ catch (System.Exception e)
78
+ {
79
+ Debug.WriteLine(e);
80
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message));
81
+ }
82
+
83
+ }
84
+
85
+ // args[0] is a NdefMessage, which is a JSON array of NdefRecords
86
+ public void writeTag(string args)
87
+ {
88
+ Debug.WriteLine("Write Tag");
89
+
90
+ try
91
+ {
92
+ string ndefMessage = JsonHelper.Deserialize<string[]>(args)[0];
93
+ NdefRecord[] records = JsonHelper.Deserialize<NdefRecord[]>(ndefMessage);
94
+ byte[] data = Ndef.toBytes(records);
95
+
96
+ stopPublishing();
97
+ publishedMessageId = proximityDevice.PublishBinaryMessage("NDEF:WriteTag", data.AsBuffer(), nfcWriteTagCallback);
98
+
99
+ // send no result now, C# callback will send success after write
100
+ PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
101
+ result.KeepCallback = true;
102
+ DispatchCommandResult(result);
103
+ }
104
+ catch (System.Exception e)
105
+ {
106
+ Debug.WriteLine(e);
107
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message));
108
+ }
109
+ }
110
+
111
+ // args[0] is a NdefMessage, which is a JSON array of NdefRecords
112
+ public void shareTag(string args)
113
+ {
114
+ Debug.WriteLine("Share Tag");
115
+
116
+ try
117
+ {
118
+ string ndefMessage = JsonHelper.Deserialize<string[]>(args)[0];
119
+ NdefRecord[] records = JsonHelper.Deserialize<NdefRecord[]>(ndefMessage);
120
+ byte[] data = Ndef.toBytes(records);
121
+
122
+ stopPublishing();
123
+ publishedMessageId = proximityDevice.PublishBinaryMessage("NDEF", data.AsBuffer());
124
+ DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
125
+ }
126
+ catch (System.Exception e)
127
+ {
128
+ Debug.WriteLine(e);
129
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message));
130
+ }
131
+ }
132
+
133
+ // no args
134
+ public void unshareTag(string args)
135
+ {
136
+ Debug.WriteLine("Unshare Tag");
137
+
138
+ try
139
+ {
140
+ stopPublishing();
141
+ DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
142
+ }
143
+ catch (System.Exception e)
144
+ {
145
+ Debug.WriteLine(e);
146
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message));
147
+ }
148
+ }
149
+
150
+ private void stopPublishing()
151
+ {
152
+ if (publishedMessageId != -1)
153
+ {
154
+ proximityDevice.StopPublishingMessage(publishedMessageId);
155
+ publishedMessageId = -1;
156
+ }
157
+ }
158
+
159
+ // MessageTransmittedHandler called after the message is written to a tag
160
+ private void nfcWriteTagCallback(ProximityDevice sender, long messageId)
161
+ {
162
+ Debug.WriteLine("Successfully wrote message to the NFC tag.");
163
+
164
+ // only write the tag one time
165
+ stopPublishing();
166
+
167
+ DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
168
+ }
169
+
170
+ private void MessageReceivedHandler(ProximityDevice sender, ProximityMessage message)
171
+ {
172
+
173
+ var bytes = message.Data.ToArray();
174
+ List<NdefRecord> records = Ndef.parse(bytes);
175
+
176
+ NfcTag tag = new NfcTag(records);
177
+
178
+ // calling a global js method to fire an nfc event
179
+ ScriptCallback script = new ScriptCallback("fireNfcTagEvent", new string[] { "ndef", JsonHelper.Serialize(tag) });
180
+ this.InvokeCustomScript(script, false);
181
+ }
182
+
183
+ [DataContract]
184
+ public class NfcTag
185
+ {
186
+ public NfcTag()
187
+ {
188
+ }
189
+
190
+ public NfcTag(List<NdefRecord> records)
191
+ {
192
+ ndefMessage = records;
193
+ }
194
+
195
+ [DataMember]
196
+ public List<NdefRecord> ndefMessage { get; set; }
197
+ }
198
+
199
+ }
200
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "com.chariotsolutions.nfc.plugin.tests",
3
+ "version": "0.0.1-dev",
4
+ "description": "Tests for phonegap-nfc",
5
+ "cordova": {
6
+ "id": "phonegap-nfc-tests",
7
+ "platforms": []
8
+ },
9
+ "keywords": [
10
+ "ecosystem:cordova"
11
+ ],
12
+ "author": "Don Coleman <don.coleman@gmail.com>",
13
+ "license": "MIT"
14
+ }
@@ -0,0 +1,11 @@
1
+ <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
2
+ xmlns:rim="http://www.blackberry.com/ns/widgets"
3
+ xmlns:android="http://schemas.android.com/apk/res/android"
4
+ id="phonegap-nfc-tests"
5
+ version="0.0.1-dev">
6
+ <name>PhoneGap NFC Plugin Tests</name>
7
+ <license>Apache 2.0</license>
8
+
9
+ <js-module src="tests.js" name="tests">
10
+ </js-module>
11
+ </plugin>