Xamarin.Forms - مثال بسيط على مضاهاة البطاقة المستندة إلى المضيف

في هذه المقالة ، سوف ننفذ ما يسمى مضاهاة البطاقة المستندة إلى المضيف (مضاهاة البطاقة البنكية على الهاتف). تحتوي الشبكة على الكثير من الأوصاف التفصيلية لهذه التكنولوجيا ، وهنا ركزت على الحصول على تطبيقات محاكي وقارئ تطبيقات وحل عدد من المشاكل العملية. نعم ، سوف تحتاج إلى جهازين مع nfc.

هناك الكثير من سيناريوهات الاستخدام: نظام المرور ، بطاقات الولاء ، بطاقات النقل ، الحصول على معلومات إضافية حول المعروضات في المتحف ، مدير كلمات المرور .

في هذه الحالة ، قد يتم أو لا يتم تشغيل التطبيق على الهاتف الذي يحاكي البطاقة ، وقد تكون شاشة هاتفك مقفلة.

بالنسبة إلى Xamarin Android ، هناك أمثلة جاهزة لمحاكي البطاقة والقارئ .
دعنا نحاول استخدام هذه الأمثلة لإنشاء تطبيقات Xamarin Forms ، ومحاكي وقارئ ، وحل المشكلات التالية فيها:

  1. عرض البيانات من المحاكي على شاشة القارئ
  2. عرض البيانات من القارئ على شاشة المحاكي
  3. يجب أن يعمل المحاكي مع تطبيق غير منشور وشاشة مقفلة
  4. ضبط إعدادات المحاكي
  5. قم بتشغيل تطبيق المحاكي عند اكتشاف قارئ
  6. التحقق من حالة محول nfc والتحول إلى إعدادات nfc

هذه المقالة تدور حول android ، لذلك ، إذا كان لديك تطبيق لنظام iOS أيضًا ، فيجب أن يكون هناك تطبيق منفصل.

الحد الأدنى من الناحية النظرية.

كما هو مكتوب في وثائق android ، بدءًا من الإصدار 4.4 (kitkat) ، تمت إضافة القدرة على محاكاة بطاقات ISO-DEP ومعالجة أوامر APDU.

يعتمد محاكاة البطاقة على خدمات Android المعروفة باسم "خدمات HCE".

عندما يقوم المستخدم بإرفاق جهاز بقارئ NFC ، يحتاج Android إلى فهم خدمة HCE التي يريد القارئ الاتصال بها. تصف المواصفة ISO / IEC 7816-4 طريقة اختيار التطبيق بناءً على معرف التطبيق (AID).

إذا كان من المثير للاهتمام الخوض في عالم جميل من صفائف البايت ، هنا وهنا المزيد عن أوامر APDU. تستخدم هذه المقالة فقط زوجين من الأوامر اللازمة لتبادل البيانات.

تطبيق القارئ


لنبدأ بالقارئ لأن إنه أبسط.

ننشئ مشروعًا جديدًا في Visual Studio مثل "Mobile App (Xamarin.Forms)" ، ثم حدد القالب "Blank" وترك علامة اختيار "Android" فقط في قسم "المنصات".

في مشروع android ، تحتاج إلى القيام بما يلي:

  • فئة CardReader - تحتوي على عدة ثوابت وأسلوب OnTagDiscovered
  • MainActivity - تهيئة فئة CardReader ، بالإضافة إلى أساليب OnPause و OnResume لتشغيل / إيقاف تشغيل القارئ عند تقليل التطبيق
  • AndroidManifest.xml - أذونات لـ nfc

وفي مشروع النظام الأساسي المشترك في ملف App.xaml.cs:

  • طريقة لعرض رسالة للمستخدم

فئة CardReader


using Android.Nfc; using Android.Nfc.Tech; using System; using System.Linq; using System.Text; namespace ApduServiceReaderApp.Droid.Services { public class CardReader : Java.Lang.Object, NfcAdapter.IReaderCallback { // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static readonly byte[] SELECT_APDU_HEADER = new byte[] { 0x00, 0xA4, 0x04, 0x00 }; // AID for our loyalty card service. private static readonly string SAMPLE_LOYALTY_CARD_AID = "F123456789"; // "OK" status word sent in response to SELECT AID command (0x9000) private static readonly byte[] SELECT_OK_SW = new byte[] { 0x90, 0x00 }; public async void OnTagDiscovered(Tag tag) { IsoDep isoDep = IsoDep.Get(tag); if (isoDep != null) { try { isoDep.Connect(); var aidLength = (byte)(SAMPLE_LOYALTY_CARD_AID.Length / 2); var aidBytes = StringToByteArray(SAMPLE_LOYALTY_CARD_AID); var command = SELECT_APDU_HEADER .Concat(new byte[] { aidLength }) .Concat(aidBytes) .ToArray(); var result = isoDep.Transceive(command); var resultLength = result.Length; byte[] statusWord = { result[resultLength - 2], result[resultLength - 1] }; var payload = new byte[resultLength - 2]; Array.Copy(result, payload, resultLength - 2); var arrayEquals = SELECT_OK_SW.Length == statusWord.Length; if (Enumerable.SequenceEqual(SELECT_OK_SW, statusWord)) { var msg = Encoding.UTF8.GetString(payload); await App.DisplayAlertAsync(msg); } } catch (Exception e) { await App.DisplayAlertAsync("Error communicating with card: " + e.Message); } } } public static byte[] StringToByteArray(string hex) => Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) .ToArray(); } } 

في وضع القراءة لمحول nfc ، عند اكتشاف البطاقة ، سيتم استدعاء طريقة OnTagDiscovered. في ذلك ، IsoDep هو كائن نتبادل به الأوامر مع الخريطة (isoDep.Transceive (command)). الأوامر صفائف البايت.

توضح الشفرة أننا نرسل سلسلة من المضاهاة تتكون من رأس SELECT_APDU_HEADER وطول معرفنا المعياري بالبايت والمعرف نفسه:

 0 164 4 0 // SELECT_APDU_HEADER 5 //  AID   241 35 69 103 137 // SAMPLE_LOYALTY_CARD_AID (F1 23 45 67 89) 

القارئ الرئيسي


هنا تحتاج إلى الإعلان عن حقل القارئ:

 public CardReader cardReader; 

وطريقتين المساعد:

 private void EnableReaderMode() { var nfc = NfcAdapter.GetDefaultAdapter(this); if (nfc != null) nfc.EnableReaderMode(this, cardReader, READER_FLAGS, null); } private void DisableReaderMode() { var nfc = NfcAdapter.GetDefaultAdapter(this); if (nfc != null) nfc.DisableReaderMode(this); } 

في طريقة OnCreate () ، قم بتهيئة القارئ وتمكين وضع القراءة:

 protected override void OnCreate(Bundle savedInstanceState) { ... cardReader = new CardReader(); EnableReaderMode(); LoadApplication(new App()); } 

وأيضًا ، قم بتمكين / تعطيل وضع القراءة عند تصغير / فتح التطبيق:

 protected override void OnPause() { base.OnPause(); DisableReaderMode(); } protected override void OnResume() { base.OnResume(); EnableReaderMode(); } 

App.xaml.cs


طريقة ثابتة لعرض رسالة:

 public static async Task DisplayAlertAsync(string msg) => await Device.InvokeOnMainThreadAsync(async () => await Current.MainPage.DisplayAlert("message from service", msg, "ok")); 

AndroidManifest.xml على


تنص وثائق android على أنه لاستخدام nfc في التطبيق الخاص بك والعمل بشكل صحيح معه ، تحتاج إلى إعلان هذه العناصر في AndroidManifest.xml:

 <uses-permission android:name="android.permission.NFC" /> <uses-sdk android:minSdkVersion="10"/>   <uses-sdk android:minSdkVersion="14"/> <uses-feature android:name="android.hardware.nfc" android:required="true" /> 

في الوقت نفسه ، إذا كان التطبيق الخاص بك يمكنه استخدام nfc ، لكن هذه ليست وظيفة مطلوبة ، فيمكنك تخطي عنصر ميزة الاستخدام والتحقق من توفر nfc أثناء التشغيل.

هذا كل شيء للقارئ.

تطبيق المحاكي


مرة أخرى ، أنشئ مشروعًا جديدًا في Visual Studio مثل "Mobile App (Xamarin.Forms)" ، ثم حدد القالب "Blank" واترك علامة الاختيار "Android" فقط في قسم "المنصات".

في مشروع Android ، قم بما يلي:

  • فئة CardService - هنا تحتاج إلى ثوابت وأسلوب ProcessCommandApdu () ، وكذلك أسلوب SendMessageToActivity ()
  • وصف الخدمة في aid_list.xml
  • رسالة آلية الإرسال في MainActivity
  • تشغيل التطبيق (إذا لزم الأمر)
  • AndroidManifest.xml - أذونات لـ nfc

وفي مشروع النظام الأساسي المشترك في ملف App.xaml.cs:

  • طريقة لعرض رسالة للمستخدم

CardService الدرجة


 using Android.App; using Android.Content; using Android.Nfc.CardEmulators; using Android.OS; using System; using System.Linq; using System.Text; namespace ApduServiceCardApp.Droid.Services { [Service(Exported = true, Enabled = true, Permission = "android.permission.BIND_NFC_SERVICE"), IntentFilter(new[] { "android.nfc.cardemulation.action.HOST_APDU_SERVICE" }, Categories = new[] { "android.intent.category.DEFAULT" }), MetaData("android.nfc.cardemulation.host_apdu_service", Resource = "@xml/aid_list")] public class CardService : HostApduService { // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static readonly byte[] SELECT_APDU_HEADER = new byte[] { 0x00, 0xA4, 0x04, 0x00 }; // "OK" status word sent in response to SELECT AID command (0x9000) private static readonly byte[] SELECT_OK_SW = new byte[] { 0x90, 0x00 }; // "UNKNOWN" status word sent in response to invalid APDU command (0x0000) private static readonly byte[] UNKNOWN_CMD_SW = new byte[] { 0x00, 0x00 }; public override byte[] ProcessCommandApdu(byte[] commandApdu, Bundle extras) { if (commandApdu.Length >= SELECT_APDU_HEADER.Length && Enumerable.SequenceEqual(commandApdu.Take(SELECT_APDU_HEADER.Length), SELECT_APDU_HEADER)) { var hexString = string.Join("", Array.ConvertAll(commandApdu, b => b.ToString("X2"))); SendMessageToActivity($"Recieved message from reader: {hexString}"); var messageToReader = "Hello Reader!"; var messageToReaderBytes = Encoding.UTF8.GetBytes(messageToReader); return messageToReaderBytes.Concat(SELECT_OK_SW).ToArray(); } return UNKNOWN_CMD_SW; } public override void OnDeactivated(DeactivationReason reason) { } private void SendMessageToActivity(string msg) { Intent intent = new Intent("MSG_NAME"); intent.PutExtra("MSG_DATA", msg); SendBroadcast(intent); } } } 

عند استلام أمر APDU من القارئ ، سيتم استدعاء أسلوب ProcessCommandApdu وسيتم نقل الأمر إليه كصفيف من وحدات البايت.

أولاً ، نتحقق من أن الرسالة تبدأ بـ SELECT_APDU_HEADER ، وإذا كان الأمر كذلك ، فقم بتكوين استجابة للقارئ. في الواقع ، يمكن أن يحدث تبادل في عدة خطوات ، والإجابة على الأسئلة والإجابة على الأسئلة ، الخ

قبل الفصل ، تصف سمة الخدمة معلمات خدمة android. عند البناء ، يحول xamarin هذا الوصف إلى مثل هذا العنصر في AndroidManifest.xml:

 <service name='md51c8b1c564e9c74403ac6103c28fa46ff.CardService' permission='android.permission.BIND_NFC_SERVICE' enabled='true' exported='true'> <meta-data name='android.nfc.cardemulation.host_apdu_service' resource='@res/0x7F100000'> </meta-data> <intent-filter> <action name='android.nfc.cardemulation.action.HOST_APDU_SERVICE'> </action> <category name='android.intent.category.DEFAULT'> </category> </intent-filter> </service> 

وصف الخدمة في aid_list.xml


في مجلد xml ، قم بإنشاء ملف aid_list.xml:

 <?xml version="1.0" encoding="utf-8"?> <host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/service_name" android:requireDeviceUnlock="false"> <aid-group android:description="@string/card_title" android:category="other"> <aid-filter android:name="F123456789"/> </aid-group> </host-apdu-service> 

توجد إشارة إليها في سمة الخدمة في فئة CardService - Resource = "@ xml / aid_list"
هنا نقوم بتعيين معرف التطبيق الخاص بنا ، والذي وفقًا لذلك سوف يصل إليه القارئ والسمة requireDeviceUnlock = "false" بحيث تتم قراءة البطاقة باستخدام شاشة غير مقفلة.

هناك @string/card_title في الكود: @string/service_name و @string/card_title . يتم الإعلان عنها في ملف القيم / strings.xml:

 <resources> <string name="card_title">My Loyalty Card</string> <string name="service_name">My Company</string> </resources> 

آلية إرسال الرسائل:


لا تحتوي الخدمة على ارتباطات بـ MainActivity ، والتي قد لا يتم تشغيلها حتى وقت استلام أمر APDU. لذلك ، نرسل رسائل من CardService إلى MainActivity باستخدام BroadcastReceiver كما يلي:

طريقة إرسال رسالة من CardService:

 private void SendMessageToActivity(string msg) { Intent intent = new Intent("MSG_NAME"); intent.PutExtra("MSG_DATA", msg); SendBroadcast(intent); } 

تلقي رسالة:
قم بإنشاء فئة MessageReceiver:

 using Android.Content; namespace ApduServiceCardApp.Droid.Services { public class MessageReceiver : BroadcastReceiver { public override async void OnReceive(Context context, Intent intent) { var message = intent.GetStringExtra("MSG_DATA"); await App.DisplayAlertAsync(message); } } } 

تسجيل MessageReceiver في MainActivity:

 protected override void OnCreate(Bundle savedInstanceState) { ... var receiver = new MessageReceiver(); RegisterReceiver(receiver, new IntentFilter("MSG_NAME")); LoadApplication(new App()); } 

App.xaml.cs


كما هو الحال في طريقة القارئ لعرض رسالة:

 public static async Task DisplayAlertAsync(string msg) => await Device.InvokeOnMainThreadAsync(async () => await Current.MainPage.DisplayAlert("message from service", msg, "ok")); 

AndroidManifest.xml على


  <uses-feature android:name="android.hardware.nfc.hce" android:required="true" /> <uses-feature android:name="FEATURE_NFC_HOST_CARD_EMULATION"/> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.BIND_NFC_SERVICE" /> <uses-sdk android:minSdkVersion="10"/>  14 

في الوقت الحالي ، لدينا بالفعل الوظائف التالية:

  • عرض البيانات من المحاكي على شاشة القارئ
  • عرض البيانات من القارئ على شاشة المحاكي
  • يجب أن يعمل المحاكي مع عدم تشغيل التطبيق ومع إيقاف تشغيل الشاشة.

التالي.

تحكم المحاكي


سأقوم بتخزين الإعدادات باستخدام Xamarin.Essentials.

لنقم بذلك: عند إعادة تشغيل تطبيق المحاكي ، سنقوم بتحديث الإعداد:

 Xamarin.Essentials.Preferences.Set("key1", Guid.NewGuid().ToString()); 

وفي طريقة ProcessCommandApdu سنتخذ هذه القيمة مرة أخرى في كل مرة:

 var messageToReader = $"Hello Reader! - {Xamarin.Essentials.Preferences.Get("key1", "key1 not found")}"; 

الآن في كل مرة تقوم فيها بإعادة تشغيل (وليس تصغير) تطبيق المضاهاة ، نرى دليل جديد ، على سبيل المثال:

 Hello Reader! - 76324a99-b5c3-46bc-8678-5650dab0529d 

أيضًا ، من خلال الإعدادات ، قم بتشغيل / إيقاف تشغيل المحاكي:

 Xamarin.Essentials.Preferences.Set("IsEnabled", false); 

وفي بداية طريقة ProcessCommandApdu أضف:

 var IsEnabled = Xamarin.Essentials.Preferences.Get("IsEnabled", false); if (!IsEnabled) return UNKNOWN_CMD_SW; // 0x00, 0x00 

هذه طريقة سهلة ، لكن هناك آخرين .

تشغيل تطبيق المحاكي عند اكتشاف قارئ


إذا كنت بحاجة فقط إلى فتح تطبيق المضاهاة ، فقم بإضافة السطر في أسلوب ProcessCommandApdu:

 StartActivity(typeof(MainActivity)); 

إذا كنت بحاجة إلى تمرير المعلمات إلى التطبيق ، ثم مثل هذا:

 var activity = new Intent(this, typeof(MainActivity)); intent.PutExtra("MSG_DATA", "data for application"); this.StartActivity(activity); 

يمكنك قراءة المعلمات التي تم تمريرها في فئة MainActivity في أسلوب OnCreate:

 ... LoadApplication(new App()); if (Intent.Extras != null) { var message = Intent.Extras.GetString("MSG_DATA"); await App.DisplayAlertAsync(message); } 

التحقق من حالة محول nfc والتحول إلى إعدادات nfc


ينطبق هذا القسم على كل من القارئ والمضاهاة.

قم بإنشاء NfcHelper في مشروع android واستخدم DependencyService للوصول إليه من رمز صفحة MainPage.

 using Android.App; using Android.Content; using Android.Nfc; using ApduServiceCardApp.Services; using Xamarin.Forms; [assembly: Dependency(typeof(ApduServiceCardApp.Droid.Services.NfcHelper))] namespace ApduServiceCardApp.Droid.Services { public class NfcHelper : INfcHelper { public NfcAdapterStatus GetNfcAdapterStatus() { var adapter = NfcAdapter.GetDefaultAdapter(Forms.Context as Activity); return adapter == null ? NfcAdapterStatus.NoAdapter : adapter.IsEnabled ? NfcAdapterStatus.Enabled : NfcAdapterStatus.Disabled; } public void GoToNFCSettings() { var intent = new Intent(Android.Provider.Settings.ActionNfcSettings); intent.AddFlags(ActivityFlags.NewTask); Android.App.Application.Context.StartActivity(intent); } } } 

الآن في المشروع عبر النظام الأساسي ، أضف واجهة INfcHelper:

 namespace ApduServiceCardApp.Services { public interface INfcHelper { NfcAdapterStatus GetNfcAdapterStatus(); void GoToNFCSettings(); } public enum NfcAdapterStatus { Enabled, Disabled, NoAdapter } } 

واستخدم كل هذا في رمز MainPage.xaml.cs:

  protected override async void OnAppearing() { base.OnAppearing(); await CheckNfc(); } private async Task CheckNfc() { var nfcHelper = DependencyService.Get<INfcHelper>(); var status = nfcHelper.GetNfcAdapterStatus(); switch (status) { case NfcAdapterStatus.Enabled: default: await App.DisplayAlertAsync("nfc enabled!"); break; case NfcAdapterStatus.Disabled: nfcHelper.GoToNFCSettings(); break; case NfcAdapterStatus.NoAdapter: await App.DisplayAlertAsync("no nfc adapter found!"); break; } } 

جيثب الروابط


منافس
قارئ

Source: https://habr.com/ru/post/ar471622/


All Articles