透過Xamarin.Forms開發Android及iOS原生NFC APP
李政輝 C.H. Lee
- 精誠資訊/恆逸教育訓練中心-資深講師
- 技術分類:Mobile行動應用開發
一、 NFC簡介
NFC代表近場通信(Near Field Communication)。該技術於2003年被國際標準組織正式接受。NFC是基於NFC論壇創建和維護的標準。NFC根據現有的射頻識別標準(RFID)來連接和交換數據。出於安全的考量,NFC的運行距離其實並不長,大約只有10CM的距離,但這也是為什麼NFC被廣泛選擇為非接觸式支付技術的主要原因之一。
- 有三種不同的NFC模式:
- 卡模擬(Card emulation)
- 讀和寫(Read and write)
- 對等通信(Peer to peer communication)
- NFC現實生活中的例子
- 使用NFC非接觸式支付的APP,如LINE Pay等。
- 實際的酒店客房卡已由可以(解鎖)門鎖的APP取代。
- Smart posters用於廣告和營銷目的。
- 醫院為患者提供內置的NFC腕帶。NFC Tag上的數據包含患者編號/標識。醫院的員工可以掃描腕帶並立即獲取患者醫療檔案。
- NFC Tag被放置在圖書館的書上。使用者可以使用圖書館應用程序掃描NFC Tag 來查看書的更多訊息。
- NFC可以用於各種目的,利用您的想像力應用在更多的生活實例。
- NFC連接
- 目標方(Target)
- 發起方(Initiator)
- NFC數據交換格式 NDEF(NFC Data Exchange Format)
- 標頭Header
- 標頭記錄包含相關的重要訊息。其中之一是類型名稱格式(Type Name Format ; TNF)。該字段指示有效負載(payload
)中的數據類型(實際傳輸的數據)。這些是TNF字段的可能值:
0 -> Empty
1 -> Well-known (text, uri 等)
2 -> Multipurpose Internet Mail Extension (MIME)
3 -> Absolute Uniform Resource Identifier (URI)
4 -> External
5- > Unknown
6 -> Unchanged (當有效負載字段中的數據太大時,該數據將在多個記錄中分塊)
7 -> Reserved (當前不使用) - 類型長度(Type length)
- 有效負載長度(Payload length)
- ID長度(ID length)
- 記錄類型(Record type)
- 記錄ID(Record ID)
- 有效負載(Payload)
實體卡已被NFC-enabled的設備取代。例如,您可以使用智能手機來打開門,而無需使用實體卡來打開酒店房間。如今,Card emulation也可用於付款。我們也稱為非接觸式付款。
NFC-enabled的設備可以通過使用標籤tags和smart posters來交換數據。NFC Tag可以容納少量設備可以讀取的數據。NFC-enabled的設備還可以在標籤上寫入數據。基本上,Smart posters可以包含多個Tag。Smart posters廣泛用於營銷/廣告目的。
確保兩個NFC-enabled的設備可以相互通信。兩個智能手機可以交換數據。
NFC適用於RFID標準。讓我們看看如何透過NFC-enabled的設備(智能手機)與NFC Tag 或smart post進行通信。通過NFC進行通信基本上有兩個部分:
目標方是NFC Tag 或smart post。那些儲存的少量數據可以由NFC-enabled的設備讀取/寫入。目標方還可以是在對等通信模式下啟用的NFC設備。
這是NFC-enabled的設備,例如智能手機。發起方啟動NFC連接。
啟動NFC連接有兩種模式:被動(passive)和主動(active)。處於被動模式時,發起方將RF能量發送到目標方以為其供電,然後目標方就可以將數據發送回發起方。在主動模式期間,發起方和目標方都擁有電源。這意味著發起方和目標方都可以相互發送數據。
當然,NFC並不是唯一用於無線發送和檢索數據的技術。RFID、低功耗藍牙、Wifi和QR碼都是可行替代方案。
NDEF格式是NFC論壇標準。此訊息格式可用於從標籤讀取或寫入,或與兩個NFC-enabled的設備一起使用。該訊息包含多個記錄:
標頭記錄中儲存的其他訊息是MB (message begin)、ME(消息結束)、CF、SR和IL。
有效負載類型的長度。
有效負載字段的長度,再次在有效負載字段中儲存實際數據。
ID字段的長度。
有效負載數據的類型,此值對應於標頭中的TNF字段。
記錄的ID,主要用於外部應用程序來標識訊息。
包含以字元(bytes)為單位儲存的實際數據。
二、 開始使用Xamarin.Forms 的Plugin.NFC
- 支援的手機平台
- Android 4.4以後的版本
- iOS 11以後的版本
步驟一:下載Plugin.NFC套件
在Solution方案按右鍵,點選[管理方案的Nuget套件],在[瀏覽]中輸入[Plugin.NFC]然後按右邊的[安裝]按鍵。如下圖所示。
(https://github.com/franckbour/Plugin.NFC)
步驟二:在Android專案Properties資料夾的AndroidManifest.xml添加NFC權限。
<uses-permission
android:name="android.permission.NFC" />
<uses-feature
android:name="android.hardware.nfc" android:required="true" />
如下圖所示:
步驟三:在Android專案的MainActivity.cs使用Plugin.NFC套件。
- 3-1. 在MainActivity類別加入IntentFilter屬性標籤初始化NFC Tag。
- 3-2. 在OnCreate方法中將CrossNFC初始化。
- 3-3. 改寫OnNewIntent方法,啟動搜尋NFC Tag。
MainActivity.cs完整程式碼如下
using System; using Android.App; using Android.Content.PM; using Android.Runtime; using Android.Views; using Android.Widget; using Android.OS; using Plugin.NFC; using Android.Content; using Android.Nfc; namespace plugin_nfc.Droid { [Activity(Label = "plugin_nfc", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] // 3-1在MainActivity類別加入IntentFilter屬性標籤初始化NFC Tag.. //需using Android.Nfc; [IntentFilter(new[] { NfcAdapter.ActionNdefDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/tw.com.companyname.plugin_nfc")] public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; base.OnCreate(savedInstanceState); //3-2. 在OnCreate方法中將CrossNFC初始化 //需using Plugin.NFC; CrossNFC.Init(this); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); LoadApplication(new App()); } //3-3. 改寫OnNewIntent方法,啟動搜尋NFC Tag protected override void OnNewIntent(Intent intent) { base.OnNewIntent(intent); // Plugin NFC: Tag Discovery Interception CrossNFC.OnNewIntent(intent); } } }
步驟四:在iOS專案的Entitlements.plist及Info.plist中添加使用NFC功能。
由於Apple的限制,僅支持NFC T Tag Reader。要在iOS設備上使用NFC,需要使用iPhone 7以後的手機和iOS 11以後的版本。
- 4-1. 在Entitlements.plist按右鍵,點選[檢查程式碼],並加入NFC Tag Reader功能。
<key>com.apple.developer.nfc.readersession.formats</key> <array> <string>NDEF</string> </array>
如下列圖示的第6行至第9行程式碼:
<key>NFCReaderUsageDescription</key> <string>NFC tag to read NDEF messages into the application</string>
如下列圖示的第6行至第7行程式碼:
步驟五:在Xamarin.Forms的共享專案中的MainPage.xaml,加入5個UI功能鍵按鈕的XAML程式碼。
MainPage.xaml完整程式碼如下:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:plugin_nfc" x:Class="plugin_nfc.MainPage"> <!--5.Add UI in MainPage.xaml--> <ScrollView> <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"> <Label Text="Plugin NFC Sample" FontSize="Large" HorizontalOptions="CenterAndExpand" /> <Button Text="Read Tag" Clicked="Button_Clicked_StartListening"/> <Button Text="Write Tag (Text)" Clicked="Button_Clicked_StartWriting" /> <Button Text="Write Tag (Uri)" Clicked="Button_Clicked_StartWriting_Uri" /> <Button Text="Write Tag (Custom)" Clicked="Button_Clicked_StartWriting_Custom" /> <Button Text="Clear Tag" Clicked="Button_Clicked_FormatTag" /> </StackLayout> </ScrollView> </ContentPage>
步驟六:在Xamarin.Forms的共享專案中的MainPage.xaml.cs,加入功能鍵程式碼。
- 6-1. 加入共用常數及變數
public const string ALERT_TITLE = "NFC"; public const string MIME_TYPE = "application/tw.com.companyname.plugin_nfc"; NFCNdefTypeFormat _type;
protected async override void OnAppearing()
{
base.OnAppearing();
//檢查平台是否支援 NFC 功能
if (CrossNFC.IsSupported)
{
//檢查 CrossNFC.Current.IsAvailable 以確認 NFC 是否可用。
if (!CrossNFC.Current.IsAvailable)
await ShowAlert("NFC is not available");
//檢查 CrossNFC.Current.IsEnabled 以確認是否啟用了 NFC。
if (!CrossNFC.Current.IsEnabled)
await ShowAlert("NFC is disabled");
//訂閱NFC事件
SubscribeEvents();
if (Device.RuntimePlatform != Device.iOS)
{
// Start NFC tag listening manually
CrossNFC.Current.StartListening();
}
}
}
void SubscribeEvents()
{
//收到 ndef 消息時引發的事件。
CrossNFC.Current.OnMessageReceived += Current_OnMessageReceived;
//發布 ndef 消息時引發的事件。
CrossNFC.Current.OnMessagePublished += Current_OnMessagePublished;
//發現標記時引發的事件。用於發布。
CrossNFC.Current.OnTagDiscovered += Current_OnTagDiscovered;
if (Device.RuntimePlatform == Device.iOS)
CrossNFC.Current.OniOSReadingSessionCancelled += Current_OniOSReadingSessionCancelled;
}
protected override bool OnBackButtonPressed() { UnsubscribeEvents(); CrossNFC.Current.StopListening(); return base.OnBackButtonPressed(); } void UnsubscribeEvents() { CrossNFC.Current.OnMessageReceived -= Current_OnMessageReceived; CrossNFC.Current.OnMessagePublished -= Current_OnMessagePublished; CrossNFC.Current.OnTagDiscovered -= Current_OnTagDiscovered; if (Device.RuntimePlatform == Device.iOS) CrossNFC.Current.OniOSReadingSessionCancelled -= Current_OniOSReadingSessionCancelled; }
async void Current_OnMessageReceived(ITagInfo tagInfo) { if (tagInfo == null) { await ShowAlert("No tag found"); return; } // Customized serial number var identifier = tagInfo.Identifier; var serialNumber = NFCUtils.ByteArrayToHexString(identifier, ":"); var title = !string.IsNullOrWhiteSpace(serialNumber) ? $"Tag [{serialNumber}]" : "Tag Info"; if (!tagInfo.IsSupported) { await ShowAlert("Unsupported tag", title); } else if (tagInfo.IsEmpty) { await ShowAlert("Empty tag", title); } else { var first = tagInfo.Records[0]; await ShowAlert(GetMessage(first), title); } } string GetMessage(NFCNdefRecord record) { var message = $"Message: {record.Message}"; message += Environment.NewLine; message += $"RawMessage: {Encoding.UTF8.GetString(record.Payload)}"; message += Environment.NewLine; message += $"Type: {record.TypeFormat.ToString()}"; if (!string.IsNullOrWhiteSpace(record.MimeType)) { message += Environment.NewLine; message += $"MimeType: {record.MimeType}"; } return message; } async void Current_OniOSReadingSessionCancelled(object sender, EventArgs e) => await ShowAlert("User has cancelled NFC reading session"); async void Current_OnMessagePublished(ITagInfo tagInfo) { try { CrossNFC.Current.StopPublishing(); if (tagInfo.IsEmpty) await ShowAlert("Formatting tag successfully"); else await ShowAlert("Writing tag successfully"); } catch (System.Exception ex) { await ShowAlert(ex.Message); } } async void Current_OnTagDiscovered(ITagInfo tagInfo, bool format) { if (!CrossNFC.Current.IsWritingTagSupported) { await ShowAlert("Writing tag is not supported on this device"); return; } try { NFCNdefRecord record = null; switch (_type) { case NFCNdefTypeFormat.WellKnown: record = new NFCNdefRecord { TypeFormat = NFCNdefTypeFormat.WellKnown, MimeType = MIME_TYPE, Payload = NFCUtils.EncodeToByteArray("This is a text message!") }; break; case NFCNdefTypeFormat.Uri: record = new NFCNdefRecord { TypeFormat = NFCNdefTypeFormat.Uri, Payload = NFCUtils.EncodeToByteArray("https://google.fr") }; break; case NFCNdefTypeFormat.Mime: record = new NFCNdefRecord { TypeFormat = NFCNdefTypeFormat.Mime, MimeType = MIME_TYPE, Payload = NFCUtils.EncodeToByteArray("This is a custom record!") }; break; default: break; } if (!format && record == null) throw new Exception("Record can't be null."); tagInfo.Records = new[] { record }; if (format) //清除標籤 CrossNFC.Current.ClearMessage(tagInfo); else { //寫標籤 CrossNFC.Current.PublishMessage(tagInfo); } } catch (System.Exception ex) { await ShowAlert(ex.Message); } }
async void Button_Clicked_StartListening(object sender, System.EventArgs e) { try { CrossNFC.Current.StartListening(); } catch (Exception ex) { await ShowAlert(ex.Message); } } void Button_Clicked_StartWriting(object sender, System.EventArgs e) => Publish(NFCNdefTypeFormat.WellKnown); void Button_Clicked_StartWriting_Uri(object sender, System.EventArgs e) => Publish(NFCNdefTypeFormat.Uri); void Button_Clicked_StartWriting_Custom(object sender, System.EventArgs e) => Publish(NFCNdefTypeFormat.Mime); void Button_Clicked_FormatTag(object sender, System.EventArgs e) => Publish(); async void Publish(NFCNdefTypeFormat? type = null) { try { if (type.HasValue) _type = type.Value; CrossNFC.Current.StartPublishing(!type.HasValue); } catch (System.Exception ex) { await ShowAlert(ex.Message); } }
Task ShowAlert(string message, string title = null) => DisplayAlert(string.IsNullOrWhiteSpace(title) ? ALERT_TITLE : title, message, "Cancel");
MainPage.xaml.cs完整程式碼如下:
using Plugin.NFC; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; namespace plugin_nfc { public partial class MainPage : ContentPage { //6.1.加入共用常數及變數 public const string ALERT_TITLE = "NFC"; public const string MIME_TYPE = "application/tw.com.companyname.plugin_nfc"; NFCNdefTypeFormat _type; public MainPage() { InitializeComponent(); } //6.2 改寫OnAppearing(),檢查平台在具有 NFC 功能啟動NFC,並訂閱NFC事件 //using Plugin.NFC; protected async override void OnAppearing() { base.OnAppearing(); //檢查平台是否支援 NFC 功能 if (CrossNFC.IsSupported) { //檢查 CrossNFC.Current.IsAvailable 以確認 NFC 是否可用。 if (!CrossNFC.Current.IsAvailable) await ShowAlert("NFC is not available"); //檢查 CrossNFC.Current.IsEnabled 以確認是否啟用了 NFC。 if (!CrossNFC.Current.IsEnabled) await ShowAlert("NFC is disabled"); //訂閱NFC事件 SubscribeEvents(); if (Device.RuntimePlatform != Device.iOS) { // Start NFC tag listening manually CrossNFC.Current.StartListening(); } } } //6.3 改寫OnBackButtonPressed(),關閉NFC 聆聽,並取消訂閱NFC事件 protected override bool OnBackButtonPressed() { UnsubscribeEvents(); CrossNFC.Current.StopListening(); return base.OnBackButtonPressed(); } //6.2加入 MessageReceived,MessagePublished ,TagDiscovered,OniOSReadingSessionCancelled事件 Handler void SubscribeEvents() { //收到 ndef 消息時引發的事件。 CrossNFC.Current.OnMessageReceived += Current_OnMessageReceived; //發布 ndef 消息時引發的事件。 CrossNFC.Current.OnMessagePublished += Current_OnMessagePublished; //發現標記時引發的事件。用於發布。 CrossNFC.Current.OnTagDiscovered += Current_OnTagDiscovered; if (Device.RuntimePlatform == Device.iOS) CrossNFC.Current.OniOSReadingSessionCancelled += Current_OniOSReadingSessionCancelled; } //6-3 取消MessageReceived,MessagePublished ,TagDiscovered,OniOSReadingSessionCancelled事件 Handler void UnsubscribeEvents() { CrossNFC.Current.OnMessageReceived -= Current_OnMessageReceived; CrossNFC.Current.OnMessagePublished -= Current_OnMessagePublished; CrossNFC.Current.OnTagDiscovered -= Current_OnTagDiscovered; if (Device.RuntimePlatform == Device.iOS) CrossNFC.Current.OniOSReadingSessionCancelled -= Current_OniOSReadingSessionCancelled; } //6-4. 完成MessageReceived,MessagePublished ,TagDiscovered,OniOSReading SessionCancelled事件 async void Current_OnMessageReceived(ITagInfo tagInfo) { if (tagInfo == null) { await ShowAlert("No tag found"); return; } // Customized serial number var identifier = tagInfo.Identifier; var serialNumber = NFCUtils.ByteArrayToHexString(identifier, ":"); var title = !string.IsNullOrWhiteSpace(serialNumber) ? $"Tag [{serialNumber}]" : "Tag Info"; if (!tagInfo.IsSupported) { await ShowAlert("Unsupported tag", title); } else if (tagInfo.IsEmpty) { await ShowAlert("Empty tag", title); } else { var first = tagInfo.Records[0]; await ShowAlert(GetMessage(first), title); } } //6.4 GetMessage方法 string GetMessage(NFCNdefRecord record) { var message = $"Message: {record.Message}"; message += Environment.NewLine; message += $"RawMessage: {Encoding.UTF8.GetString(record.Payload)}"; message += Environment.NewLine; message += $"Type: {record.TypeFormat.ToString()}"; if (!string.IsNullOrWhiteSpace(record.MimeType)) { message += Environment.NewLine; message += $"MimeType: {record.MimeType}"; } return message; } async void Current_OniOSReadingSessionCancelled(object sender, EventArgs e) => await ShowAlert("User has cancelled NFC reading session"); async void Current_OnMessagePublished(ITagInfo tagInfo) { try { CrossNFC.Current.StopPublishing(); if (tagInfo.IsEmpty) await ShowAlert("Formatting tag successfully"); else await ShowAlert("Writing tag successfully"); } catch (System.Exception ex) { await ShowAlert(ex.Message); } } async void Current_OnTagDiscovered(ITagInfo tagInfo, bool format) { if (!CrossNFC.Current.IsWritingTagSupported) { await ShowAlert("Writing tag is not supported on this device"); return; } try { NFCNdefRecord record = null; switch (_type) { case NFCNdefTypeFormat.WellKnown: record = new NFCNdefRecord { TypeFormat = NFCNdefTypeFormat.WellKnown, MimeType = MIME_TYPE, Payload = NFCUtils.EncodeToByteArray("This is a text message!") }; break; case NFCNdefTypeFormat.Uri: record = new NFCNdefRecord { TypeFormat = NFCNdefTypeFormat.Uri, Payload = NFCUtils.EncodeToByteArray("https://google.fr") }; break; case NFCNdefTypeFormat.Mime: record = new NFCNdefRecord { TypeFormat = NFCNdefTypeFormat.Mime, MimeType = MIME_TYPE, Payload = NFCUtils.EncodeToByteArray("This is a custom record!") }; break; default: break; } if (!format && record == null) throw new Exception("Record can't be null."); tagInfo.Records = new[] { record }; if (format) //清除標籤 CrossNFC.Current.ClearMessage(tagInfo); else { //寫標籤 CrossNFC.Current.PublishMessage(tagInfo); } } catch (System.Exception ex) { await ShowAlert(ex.Message); } } //6.5 完成5個UI功能鍵按鈕的click事件程式碼 async void Button_Clicked_StartListening(object sender, System.EventArgs e) { try { CrossNFC.Current.StartListening(); } catch (Exception ex) { await ShowAlert(ex.Message); } } void Button_Clicked_StartWriting(object sender, System.EventArgs e) => Publish(NFCNdefTypeFormat.WellKnown); void Button_Clicked_StartWriting_Uri(object sender, System.EventArgs e) => Publish(NFCNdefTypeFormat.Uri); void Button_Clicked_StartWriting_Custom(object sender, System.EventArgs e) => Publish(NFCNdefTypeFormat.Mime); void Button_Clicked_FormatTag(object sender, System.EventArgs e) => Publish(); //6.5 Publish async void Publish(NFCNdefTypeFormat? type = null) { try { if (type.HasValue) _type = type.Value; CrossNFC.Current.StartPublishing(!type.HasValue); } catch (System.Exception ex) { await ShowAlert(ex.Message); } } //6.6 完成ShowAlert共用方法,使用 DisplayAlert對話方塊 Task ShowAlert(string message, string title = null) => DisplayAlert(string.IsNullOrWhiteSpace(title) ? ALERT_TITLE : title, message, "Cancel"); // void Debug(string message) => System.Diagnostics.Debug.WriteLine(message); } }
程式碼主要功能說明如下:
- 讀取標籤
- 開始聆聽CrossNFC.Current.StartListening()
- 收到NDEF訊息後,引發OnMessageReceived事件
- 寫標籤
- 透過5個UI功能鍵按鈕的click事件呼叫 CrossNFC.Current.StartPublishing()
- 在OnTagDiscovered事件時透過CrossNFC.Current.PublishMessage(ITagInfo)編寫標籤
- 清除標籤
- 要清除標籤,請在OnTagDiscovered事件透過CrossNFC.Current.ClearMessage(ITagInfo) 清除標籤
- 要清除標籤不要忘了在OnMessagePublished事件引發時呼叫CrossNFC.Current.StopPublishing()方法
原生APP執行畫面: