#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?
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 & App Links
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:
- 🚫 adrynov/Capacitor-NFC-Plugin - ostatni raz aktualizowany 5 lat temu, średniej jakości kod
- 🚫 chariotsolutions/phonegap-nfc - plugin do PhoneGapa, ostatni raz aktualizowany 4 lata temu, wygląda trochę lepiej, licencja MIT, ale brak aktywności w repo
- 🚫 NePheus/capacitor-nfc-launch - kod wygląda lepiej, ostatnia aktywność kilka miesięcy temu, ale brak wsparcia dla iOS, odpada
- ✅ 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
dlahttps://
- obecnie nie znalazłem tego w paczcecapacitor-nfc
, ale zgłoszę to twórcomlog
- funkcja służy do logowania zdarzeń w konsoli Logcat (Android) lub XCode (iOS) - serializacja obiektów do JSONaisValidUriRecord
- funkcja sprawdza czy rekord jest poprawnym rekordem URINfc.addListener
- nasłuchuje zdarzenianfcTagScanned
i wywołuje callbackonScanned
- tutaj dzieje się trochę więcej:record
- pobieramy pierwszy rekord z wiadomości (NdefMessage
), dla tego przykładu zakładamy, że jest tylko jeden rekordisValidUriRecord
- sprawdzamy czy rekord jest poprawnym rekordem URIpayload
- pobieramy payload z rekordu, payload jest to tablica bajtów, gdzie pierwszy bajt to kod identyfikujący URIbytesView
- tworzymy widok na tablicę bajtów, aby móc zdekodować URIstr
- dekodujemy tablicę bajtów do stringaprefix
- pobieramy prefix URI na podstawie kodu identyfikującego URIuri
- tworzymy pełny URIurl
- tworzymy obiekt URL, który pozwala na walidację URIwindow.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.