#note CapacitorJS + deep links + nfc - przypadek użycia post

Od czasu do czasu zajmuję się utrzymaniem aplikacji mobilnej na Androida i iOSa. Obecnie aplikacja napisana jest w CapacitorJS & TypeScript. Pod spodem w całym procesie tworzenia wykorzystuję ReactJS, WebPack, ESLint etc. Obie platformy wspierają kilka dodatkowych pluginów jak InAppBrowser, Camera, Push Notifications, FileOpener2 etc.

Teraz wyobraź sobie dużą konferencję IT, gdzie zamiast etykiet, tablic informacyjnych, kodów QR umieścić zaprogramowane tagi NFC, a każdy uczestnik mógłby automatycznie je zeskanować i dostać adekwatny do swoich preferencji materiał. Czy da się to osiągnąć za pomocą aplikacji mobilnej napisanej w JavaScript?

Click here to read English version (Google Translate)

CapacitorJS

CapacitorJS to framework, który pozwala na tworzenie aplikacji mobilnych w JavaScript. CapacitorJS jest rozwijany przez Ionic Team i pozwala na wykorzystanie natywnych funkcji urządzenia jak kamera, NFC, GPS, Bluetooth, Push Notifications etc.

Skoro jednak mowa o natywnych funkcjach, w tym wpisie opiszę jak wykorzystać NFC w aplikacji mobilnej napisanej w CapacitorJS.

Przypadek użycia

Wyobraź sobie konferencję IT, gdzie wchodząc na salę skanujesz tagi NFC, które automatycznie otwierają stronę internetową z materiałami z prezentacji, agendą, informacjami o prelegentach etc.

Czy da się to osiągnąć za pomocą aplikacji mobilnej napisanej w JavaScript? Oczywiście, że tak!

By to zrealizować musimy poskładać do kupy kilka klocków: App Links, Deep Links, NFC Tags.

Deep Links to mechanizm, który pozwala na uruchomienie aplikacji mobilnej po kliknięciu w link. W przypadku Androida, linki te nazywają się App Links. W przypadku iOSa, linki te nazywają się Universal Links.

Integrując DeepLinks pozwalamy aplikacji mobilnej na otwieranie adresów URL w aplikacji mobilnej np. https://michal.kruczek.it

Całego procesu konfiguracji nie będę tutaj opisywał, ponieważ w dokumentacji Capacitora, Google'a i Apple'a jest to opisane bardzo dobrze.

Wystarczy podążać za https://capacitorjs.com/docs/guides/deep-links

Tagi NFC

Tagi NFC to małe urządzenia, które można zaprogramować do wykonywania różnych akcji. W tym przypadku chcemy, by po zeskanowaniu tagu NFC otwierała się strona internetowa z materiałami z prezentacji, agendą, informacjami o prelegentach etc.

Taki najprościej zakodować przy użyciu aplikacji mobilnej np.:

CapacitorJS NFC

CapacitorJS nie wspiera tagów NFC, ale dzięki architekturze pozwalającej na rozszerzenia możemy skorzystać z pluginów. Szybki przegląd:

  1. 🚫 adrynov/Capacitor-NFC-Plugin - ostatni raz aktualizowany 5 lat temu, średniej jakości kod
  2. 🚫 chariotsolutions/phonegap-nfc - plugin do PhoneGapa, ostatni raz aktualizowany 4 lata temu, wygląda trochę lepiej, licencja MIT, ale brak aktywności w repo
  3. 🚫 NePheus/capacitor-nfc-launch - kod wygląda lepiej, ostatnia aktywność kilka miesięcy temu, ale brak wsparcia dla iOS, odpada
  4. capawesome-team/capacitor-nfc - plugin płatny, jakość kodu bez zastrzeżeń, dostęp po wsparciu projektu niemalże natychmiastowy, feedback na zgłoszone problemy pomimo okresu świątecznego kilkugodzinny, wsparcie dla iOS i Androida, wsparcie dla NDEF_DISCOVERED

Po kilkunastu godzinach analizy i testowania wszystkich pluginów, tylko ten ostatnio spełnił oczekiwania pozwalając na konfigurację i wykorzystanie tagów NFC do uruchomienia aplikacji mobilnej.

NFC Launcher App

Na potrzeby tego wpisu stworzyłem aplikację mobilną pokazującą prosty przykład wspierający Deep Links & NFC tags.

Link do aplikacji: https://github.com/partikus/capacitor-nfc-launcher

‼️ Testowanie tagów NFC ‼️

Tutaj natrafiłem na największy problem. Spędzając wiele godzin, testując różne obejścia i próbując przy użyciu adb uruchomić aplikację z tagiem NFC.

np. https://stackoverflow.com/questions/71074064/how-attach-tag-intent-extra-for-nfc-tag-scan-simulation-through-adb-shell

adb shell am start \
    -W -a android.nfc.action.NDEF_DISCOVERED -d "https://kruczek.it/about" \
    --es android.nfc.extra.TAG 'https:/kruczek.it/about' \
     it.kruczek.nfc.launcher

Okazało się, że to wszystko nie ma sensu, ponieważ adb nie wspiera wysyłania NFC intents. Można użyć adb shell am do wysyłania akcji, ale nie wspiera wysyłania NDEF_DISCOVERED wraz z obiektem Tag. Jedynym sposobem na wysłanie NFC z obiektem Tag jest użycie innej aplikacji do wysyłania takiego zdarzenia.

Powyższe repozytorium zawiera dodatkową aktywność NFCActivity, która pozwala na emulowanie tego zdarzenia. Jednakże, powinna to robić inna aplikacja. Wystarczy, że w głównej aktywności (Android Activity) dodamy obsługę NFC i wyślemy zdarzenie do aplikacji.

public void dispatchNDEF() {
    final Uri uri = Uri.parse("https://kruczek.it/about");

    final Intent intent = new Intent(NfcAdapter.ACTION_NDEF_DISCOVERED, uri);

    intent.setPackage("it.kruczek.nfc.launcher");
    intent.setData(uri);
    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);

    Parcel parcel = Parcel.obtain();

    byte[] messages = uri.toString().getBytes();

    parcel.writeByteArray(messages);
    Tag tag = Tag.CREATOR.createFromParcel(parcel);

    NdefRecord record = NdefRecord.createUri(uri.toString());
    NdefMessage message = new NdefMessage(new NdefRecord[]{record});

    intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[]{message});
    intent.putExtra(NfcAdapter.EXTRA_TAG, tag);

    startActivity(intent);
}

Powyższy snippet tworzy zdarzenie NFC i je wysyła. Tag NFC w tym wypadku zawiera NdefRecord.

Obsługa NFC w aplikacji

Zgodnie z dokumentacją pluginu, do nasłuchiwania eventów NFC trzeba dodać nfcTagScanned listener. Tak też zróbmy. Ale honohono, co to jest NdefMessage? Jak zdekodować poprawnie wiadomość NdefRecord?

Tutaj odsyłam do linków na samym końcu. Ja wolę przejść do efektu finalnego.


// @see https://www.oreilly.com/library/view/beginning-nfc/9781449324094/ch04.html export const UriIdentifierCodeMapping: Partial< Record<UriIdentifierCode, string> > = { [UriIdentifierCode.Http]: "http://", [UriIdentifierCode.Https]: "https://", [UriIdentifierCode.HttpWww]: "http://www.", [UriIdentifierCode.HttpsWww]: "https://www.", [UriIdentifierCode.Tel]: "tel://", // ... and more // @see https://austinblackstoneengineering.com/nfc-p2p-basics/ }; const log = (msg, ...contexts) => console.trace(`[app] ${msg}`, JSON.stringify(contexts, null, 2)); const isValidUriRecord = (record?: NdefRecord): boolean => { if (!record) { return false; } // checking record type if (record?.tnf !== TypeNameFormat.AbsoluteUri) { return false; } if (Array.isArray(record?.payload) && record?.payload?.length < 2) { return false; } return record?.payload?.[0] === UriIdentifierCode.Https; }; const onScanned = async (event: NfcTagScannedEvent) => { log("NFC tag scanned", event); try { // taking a first record from the message const record = event?.nfcTag?.message?.records?.[0]; if (!isValidUriRecord(record)) { return; } const payload = record?.payload as [UriIdentifierCode, ...number[]]; // payload is an array of bytes, the first byte is the Uri Identifier Code const bytesView = new Uint8Array(payload.slice(1)); log("an array of 8-bit unsigned integers", bytesView); // convert bytes to string, encoding can be specfied, defaults to utf-8 const str = new TextDecoder().decode(bytesView); log("decoded uri string", str); const prefix = UriIdentifierCodeMapping?.[payload[0]]; if (!prefix) { const supportedCodes = JSON.stringify( Object.keys(UriIdentifierCodeMapping) ); throw new Error( [ `unknown uri identifier code, only "${supportedCodes}" are supported, ${payload[0]} given`, `@see https://www.oreilly.com/library/view/beginning-nfc/9781449324094/ch04.html`, `@see https://austinblackstoneengineering.com/nfc-p2p-basics/`, ].join("\n") ); } const uri = `${prefix}${str.trim()}`; log("full uri incl. scheme", uri); const url = new URL(uri); // use InAppBrowser or Browser.open to open the url window.open(url.toString(), "_blank"); } catch (e) { log("error", e); } }; Nfc.addListener("nfcTagScanned", onScanned);
  • UriIdentifierCodeMapping - mapa kodów identyfikujących URI tj. 0x04 dla https:// - obecnie nie znalazłem tego w paczce capacitor-nfc, ale zgłoszę to twórcom
  • log - funkcja służy do logowania zdarzeń w konsoli Logcat (Android) lub XCode (iOS) - serializacja obiektów do JSONa
  • isValidUriRecord - funkcja sprawdza czy rekord jest poprawnym rekordem URI
  • Nfc.addListener - nasłuchuje zdarzenia nfcTagScanned i wywołuje callback
  • onScanned - tutaj dzieje się trochę więcej:
    • record - pobieramy pierwszy rekord z wiadomości (NdefMessage), dla tego przykładu zakładamy, że jest tylko jeden rekord
    • isValidUriRecord - sprawdzamy czy rekord jest poprawnym rekordem URI
    • payload - pobieramy payload z rekordu, payload jest to tablica bajtów, gdzie pierwszy bajt to kod identyfikujący URI
    • bytesView - tworzymy widok na tablicę bajtów, aby móc zdekodować URI
    • str - dekodujemy tablicę bajtów do stringa
    • prefix - pobieramy prefix URI na podstawie kodu identyfikującego URI
    • uri - tworzymy pełny URI
    • url - tworzymy obiekt URL, który pozwala na walidację URI
    • window.open - otwieramy URL w nowym oknie

Podsumowanie

Dzięki obsłudze Deep Links & NFC tag, możemy zakodować dowolny dane na tagu NFC i otworzyć je w aplikacji mobilnej. W tym przypadku wykorzystałem to do otwarcia strony internetowej (Absolute URI), ale można to wykorzystać do uruchomienia dowolnej aplikacji lub wywołania dowolnej akcji.

Linki

Kategorie: notes, tools

Tagi: notes, note, notatki, notatka, capacitor, nfc, deeplinks, android, ios