Skip to content

Commit

Permalink
NFC card emulation (#100)
Browse files Browse the repository at this point in the history
* - NFC card emulation mode for sharing DCC on View cert page. Host apdu service with NFC Forum type 4 implementation for NDEF message exchange;
- NFC reader mode on main certificates page;

* - add base binding fragment;
- clean up logic;

* - claim certificate when received via NFC;

* - detect and handle DCC sent event in service;
- display toast when dcc sent;

* - code refactor;

* - remove NFC reader initialization from cert fragment;
- remove UI component;
- parse message in activity and navigate to claim cert fragment;
- send qr code text only via nfc;

* - navigate to claim cert view on app start;

* - merge main branch;
- remove unused images;

* - revert changes to claim viewModel;

* - update button style;
  • Loading branch information
MykhailoNester authored Aug 25, 2021
1 parent dd0219f commit ea6266c
Show file tree
Hide file tree
Showing 39 changed files with 852 additions and 83 deletions.
16 changes: 0 additions & 16 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.NFC" />

<application
android:name=".DgcaWalletApplication"
Expand All @@ -15,12 +16,21 @@
android:supportsRtl="true"
android:theme="@style/Theme.DgcaWalletAppAndroid">

<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 All @@ -33,6 +43,19 @@
android:exported="false"
tools:node="remove" />

<service
android:name=".nfc.DCCApduService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/nfc_application_ids" />
</service>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
Expand Down
65 changes: 62 additions & 3 deletions app/src/main/java/dgca/wallet/app/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,61 @@

package dgca.wallet.app.android

import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import dagger.hilt.android.AndroidEntryPoint
import dgca.wallet.app.android.certificate.CertificatesFragmentDirections
import dgca.wallet.app.android.databinding.ActivityMainBinding
import dgca.wallet.app.android.nfc.NdefParser
import timber.log.Timber

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

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

val appBarConfiguration = AppBarConfiguration(
topLevelDestinationIds = setOf(),
fallbackOnNavigateUpListener = ::onSupportNavigateUp
)
binding.toolbar.setupWithNavController(navController, appBarConfiguration)

setSupportActionBar(binding.toolbar)

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

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

checkNdefMessage(intent)
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
Expand All @@ -62,8 +85,6 @@ class MainActivity : AppCompatActivity() {
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
return when (item.itemId) {
R.id.settings -> {
navController.navigate(R.id.settingsFragment)
Expand All @@ -74,11 +95,49 @@ class MainActivity : AppCompatActivity() {
}
}

override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}

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

fun disableBackButton() {
binding.toolbar.navigationIcon = null
}

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()) {
val action = CertificatesFragmentDirections.actionCertificatesFragmentToClaimCertificateFragment(qrCodeText)
navController.navigate(action)
} else {
Timber.d("Received empty NDEFMessage")
}
}
}
58 changes: 58 additions & 0 deletions app/src/main/java/dgca/wallet/app/android/base/BindingFragment.kt
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 18/08/2021, 17:21
*/

package dgca.wallet.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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,41 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import dgca.wallet.app.android.MainActivity
import dgca.wallet.app.android.base.BindingFragment
import dgca.wallet.app.android.databinding.FragmentCertificatesBinding
import java.io.File

@AndroidEntryPoint
class CertificatesFragment : Fragment(), CertificateCardsAdapter.CertificateCardClickListener,
class CertificatesFragment : BindingFragment<FragmentCertificatesBinding>(),
CertificateCardsAdapter.CertificateCardClickListener,
CertificateCardsAdapter.FileCardClickListener {

private val viewModel by viewModels<CertificatesViewModel>()
private var _binding: FragmentCertificatesBinding? = null
private val binding get() = _binding!!

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activity as MainActivity).clearBackground()
}

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

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

(activity as MainActivity).disableBackButton()
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { requireActivity().finish() }
binding.scanCode.setOnClickListener { showAddNewDialog() }

viewModel.certificates.observe(viewLifecycleOwner, {
setCertificateCards(it)
})
viewModel.inProgress.observe(viewLifecycleOwner, {
binding.progressView.isVisible = it
})
viewModel.certificates.observe(viewLifecycleOwner, { setCertificateCards(it) })
viewModel.inProgress.observe(viewLifecycleOwner, { binding.progressView.isVisible = it })

viewModel.fetchCertificates()

setFragmentResultListener(AddNewBottomDialogFragment.REQUEST_KEY) { key, bundle ->
Expand Down Expand Up @@ -104,11 +99,6 @@ class CertificatesFragment : Fragment(), CertificateCardsAdapter.CertificateCard
}
}

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

private fun showCodeReader() {
val action = CertificatesFragmentDirections.actionCertificatesFragmentToCodeReaderFragment()
findNavController().navigate(action)
Expand Down
Loading

0 comments on commit ea6266c

Please sign in to comment.