Skip to content

Commit

Permalink
Feature/nfc reader (#192)
Browse files Browse the repository at this point in the history
- add nfc functionality;
- handle nfc message in code reader fragment;
- add base fragment with binding;
  • Loading branch information
MykhailoNester authored Sep 1, 2021
1 parent 0f8e558 commit 62530cb
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 30 deletions.
12 changes: 11 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,22 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DgcaVerifierAppAndroid">
<activity android:name=".MainActivity">

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>

<activity
Expand Down
44 changes: 23 additions & 21 deletions app/src/main/java/dgca/verifier/app/android/CodeReaderFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import android.widget.AdapterView.OnItemSelectedListener
import androidx.activity.addCallback
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.NavDestination
Expand All @@ -45,23 +44,22 @@ import com.journeyapps.barcodescanner.BarcodeCallback
import com.journeyapps.barcodescanner.BarcodeResult
import com.journeyapps.barcodescanner.DefaultDecoderFactory
import dagger.hilt.android.AndroidEntryPoint
import dgca.verifier.app.android.base.BindingFragment
import dgca.verifier.app.android.databinding.FragmentCodeReaderBinding
import dgca.verifier.app.engine.data.source.countries.COUNTRIES_MAP
import java.util.*

import timber.log.Timber
import java.util.Locale

private const val CAMERA_REQUEST_CODE = 1003

@AndroidEntryPoint
class CodeReaderFragment : Fragment(), NavController.OnDestinationChangedListener {

private var _binding: FragmentCodeReaderBinding? = null
private val binding get() = _binding!!
class CodeReaderFragment : BindingFragment<FragmentCodeReaderBinding>(), NavController.OnDestinationChangedListener {

private val viewModel by viewModels<CodeReaderViewModel>()

private lateinit var beepManager: BeepManager
private var lastText: String? = null
private var refinedCountries: List<String> = emptyList()

private val callback: BarcodeCallback = object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
Expand All @@ -86,14 +84,8 @@ class CodeReaderFragment : Fragment(), NavController.OnDestinationChangedListene
(activity as MainActivity).clearBackground()
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCodeReaderBinding.inflate(inflater, container, false)
return binding.root
}
override fun onCreateBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCodeReaderBinding =
FragmentCodeReaderBinding.inflate(inflater, container, false)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -112,11 +104,6 @@ class CodeReaderFragment : Fragment(), NavController.OnDestinationChangedListene
setUpCountriesProcessing()
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

override fun onResume() {
super.onResume()
findNavController().addOnDestinationChangedListener(this)
Expand All @@ -135,7 +122,7 @@ class CodeReaderFragment : Fragment(), NavController.OnDestinationChangedListene
View.GONE
} else {
val countries = pair.first
val refinedCountries =
refinedCountries =
countries.sortedBy { Locale("", COUNTRIES_MAP[it] ?: it).displayCountry }
binding.countrySelector.adapter = CountriesAdapter(refinedCountries, layoutInflater)
if (pair.second!!.isNotBlank()) {
Expand Down Expand Up @@ -197,4 +184,19 @@ class CodeReaderFragment : Fragment(), NavController.OnDestinationChangedListene
lastText = ""
}
}

fun onNdefMessageReceived(qrCodeText: String) {
val position = binding.countrySelector.selectedItemPosition
if (position == -1 || refinedCountries.isEmpty()) {
return
}

try {
val countryCode = refinedCountries[position].toLowerCase(Locale.ROOT)
val action = CodeReaderFragmentDirections.actionCodeReaderFragmentToVerificationFragment(qrCodeText, countryCode)
findNavController().navigate(action)
} catch (ex: Exception) {
Timber.d("Cannot get iso country code for position.")
}
}
}
61 changes: 61 additions & 0 deletions app/src/main/java/dgca/verifier/app/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,83 @@

package dgca.verifier.app.android

import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import dagger.hilt.android.AndroidEntryPoint
import dgca.verifier.app.android.nfc.NdefParser
import timber.log.Timber

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

private lateinit var navHostFragment: NavHostFragment
private lateinit var navController: NavController

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.activity_main)

navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController

navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.codeReaderFragment) {
checkNdefMessage(intent)
}
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
checkNdefMessage(intent)
}

fun clearBackground() {
window.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.white))
}

private fun checkNdefMessage(intent: Intent) {
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)?.also { rawMessages ->
val messages: List<NdefMessage> = rawMessages.map { it as NdefMessage }
parseNdefMessages(messages)
intent.removeExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
}
}
}

private fun parseNdefMessages(messages: List<NdefMessage>) {
if (messages.isEmpty()) {
return
}

val builder = StringBuilder()
val records = NdefParser.parse(messages[0])
val size = records.size

for (i in 0 until size) {
val record = records[i]
val str = record.str()
builder.append(str)
}

val qrCodeText = builder.toString()
if (qrCodeText.isNotEmpty()) {
navHostFragment.childFragmentManager.primaryNavigationFragment?.let { fragment ->
if (fragment is CodeReaderFragment && fragment.isVisible) {
fragment.onNdefMessageReceived(qrCodeText)
}
}
} else {
Timber.d("Received empty NDEFMessage")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* ---license-start
* eu-digital-green-certificates / dgca-verifier-app-android
* ---
* Copyright (C) 2021 T-Systems International GmbH and all other contributors
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*
* Created by mykhailo.nester on 25/08/2021, 15:19
*/

package dgca.verifier.app.android.base

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding

abstract class BindingFragment<T : ViewBinding> : Fragment() {

private var _binding: T? = null
val binding get() = _binding!!

abstract fun onCreateBinding(inflater: LayoutInflater, container: ViewGroup?): T

open fun onDestroyBinding(binding: T) {
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val innerBinding = onCreateBinding(inflater, container)
_binding = innerBinding
return innerBinding.root
}

override fun onDestroyView() {
val innerBinding = _binding
if (innerBinding != null) {
onDestroyBinding(innerBinding)
}

_binding = null

super.onDestroyView()
}
}
83 changes: 83 additions & 0 deletions app/src/main/java/dgca/verifier/app/android/nfc/NdefParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* ---license-start
* eu-digital-green-certificates / dgca-verifier-app-android
* ---
* Copyright (C) 2021 T-Systems International GmbH and all other contributors
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*
* Created by mykhailo.nester on 17/08/2021, 18:52
*/

package dgca.verifier.app.android.nfc

import android.nfc.NdefMessage
import android.nfc.NdefRecord
import timber.log.Timber
import java.io.UnsupportedEncodingException
import java.util.*
import kotlin.experimental.and

object NdefParser {

fun parse(message: NdefMessage): List<ParsedNdefRecord> = getRecords(message.records)

private fun getRecords(records: Array<NdefRecord>): List<ParsedNdefRecord> =
records.map {
it.parse() ?: object : ParsedNdefRecord {
override fun str(): String {
return String(it.payload)
}
}
}
}

fun NdefRecord.parse(): ParsedNdefRecord? {
return if (tnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(type, NdefRecord.RTD_TEXT)) {
try {
val recordPayload = payload

/*
* payload[0] contains the "Status Byte Encodings" field, per the
* NFC Forum "Text Record Type Definition" section 3.2.1.
*
* bit7 is the Text Encoding Field.
*
* if (Bit_7 == 0): The text is encoded in UTF-8 if (Bit_7 == 1):
* The text is encoded in UTF16
*
* Bit_6 is reserved for future use and must be set to zero.
*
* Bits 5 to 0 are the length of the IANA language code.
*/
val textEncoding = if (recordPayload[0] and 128.toByte() == 0.toByte()) {
Charsets.UTF_8
} else {
Charsets.UTF_16
}

val languageCodeLength: Int = (recordPayload[0] and 63.toByte()).toInt()
val text = String(
recordPayload, languageCodeLength + 1,
recordPayload.size - languageCodeLength - 1, textEncoding
)
return TextRecord(text)
} catch (e: UnsupportedEncodingException) {
Timber.w("We got a malformed tag.")
return null
}
} else {
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* ---license-start
* eu-digital-green-certificates / dgca-verifier-app-android
* ---
* Copyright (C) 2021 T-Systems International GmbH and all other contributors
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*
* Created by mykhailo.nester on 17/08/2021, 18:52
*/

package dgca.verifier.app.android.nfc

interface ParsedNdefRecord {
fun str(): String
}
Loading

0 comments on commit 62530cb

Please sign in to comment.