Estonian-ID-card-mobile-aut.../MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt

371 lines
16 KiB
Kotlin
Raw Normal View History

package com.tarkvaraprojekt.mobileauthapp
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
2021-11-25 14:17:00 +02:00
import android.nfc.NfcAdapter
import android.nfc.TagLostException
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
2021-12-03 16:11:48 +02:00
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
2021-12-03 16:11:48 +02:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2021-12-06 22:08:04 +02:00
import com.koushikdutta.ion.Ion
2021-11-25 14:17:00 +02:00
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import org.json.JSONObject
import java.lang.Exception
2021-12-06 22:08:04 +02:00
import java.lang.RuntimeException
import java.net.URL
/**
* HomeFragment is only shown to the user when then the user launches the application. When the application
* is launched by another application or a website then this Fragment will be skipped.
* This fragment uses the fields from the MainActivity by casting the activity to MainActivity.
* This might not be the best practice, but the application uses a single activity design so it should
* always work.
*/
class HomeFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private val intentParams: ParametersViewModel by activityViewModels()
2021-12-04 17:21:07 +02:00
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
2021-11-25 14:17:00 +02:00
// The ID card reader mode is enabled on the home fragment when can is saved.
private var canSaved: Boolean = false
// Is the app used for authentication
private var auth: Boolean = false
private var receiver: BroadcastReceiver? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
2021-12-04 17:21:07 +02:00
_binding = FragmentHomeBinding.inflate(inflater, container, false)
// Making settings menu active again
(activity as MainActivity).menuAvailable = true
2021-12-04 17:21:07 +02:00
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialChecks()
if (requireActivity().intent.data?.getQueryParameter("action") != null) {
// Currently we only support authentication not signing.
auth = true
}
2021-12-04 17:21:07 +02:00
val mobile = requireActivity().intent.getBooleanExtra("mobile", false)
if (auth || mobile) {
startAuthentication(mobile)
} else {
receiver = object : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
updateAction(canSaved)
}
}
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
requireActivity().registerReceiver(receiver, filter)
updateAction(canSaved)
}
}
/**
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
*/
private fun goToTheNextFragment(mobile: Boolean = false) {
(activity as MainActivity).menuAvailable = false
val action =
HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
findNavController().navigate(action)
}
/**
* Method that starts the authentication use case.
2021-11-25 18:09:45 +02:00
*
* NOTE: Comment out try-catch block when testing without backend
*/
private fun startAuthentication(mobile: Boolean) {
try {
if (mobile) {
// We use !! to get extras because we want an exception to be thrown when something is missing.
2021-12-14 22:46:35 +02:00
//intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
2021-12-14 22:46:35 +02:00
val challengeUrl = requireActivity().intent.getStringExtra("challenge")!!
val headers = requireActivity().intent.getStringExtra("headers")!!
val map: HashMap<String, String> = HashMap()
map.put("sessionId", headers)
intentParams.setHeaders(map)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
Ion.with(activity)
.load(challengeUrl)
.setHeader("sessionId", headers)
.asJsonObject()
.setCallback { _, result ->
try {
val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
intentParams.setChallenge(challenge)
goToTheNextFragment(mobile)
} catch (e: Exception) {
Log.i("GETrequest", e.toString())
}
}
} else { //Website
2021-12-06 22:08:04 +02:00
/*
var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!!
// TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back.
challenge = challenge.replace(" ", "+")
intentParams.setChallenge(challenge)
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!)
2021-12-06 22:08:04 +02:00
*/
var getAuthChallengeUrl =
requireActivity().intent.data!!.getQueryParameter("getAuthChallengeUrl")!!
getAuthChallengeUrl =
getAuthChallengeUrl.substring(1, getAuthChallengeUrl.length - 1)
var postAuthTokenUrl =
requireActivity().intent.data!!.getQueryParameter("postAuthTokenUrl")!!
2021-12-06 23:27:43 +02:00
postAuthTokenUrl = postAuthTokenUrl.substring(1, postAuthTokenUrl.length - 1)
val headers =
getHeaders(requireActivity().intent.data!!.getQueryParameter("headers")!!)
2021-12-06 22:08:04 +02:00
intentParams.setAuthUrl(postAuthTokenUrl)
2021-12-06 23:27:43 +02:00
val address = "https://" + URL(getAuthChallengeUrl).host
intentParams.setOrigin(address)
2021-12-06 22:08:04 +02:00
intentParams.setHeaders(headers)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
val ion = Ion.with(activity)
2021-12-06 22:08:04 +02:00
.load(getAuthChallengeUrl)
// Set headers.
for ((header, value) in intentParams.headers) {
ion.setHeader(header, value)
}
ion
2021-12-06 22:08:04 +02:00
.asJsonObject()
.setCallback { _, result ->
try {
// Get data from the result and call launchAuth method
val challenge =
result.asJsonObject["nonce"].toString().replace("\"", "")
2021-12-06 22:08:04 +02:00
intentParams.setChallenge(challenge)
goToTheNextFragment(mobile)
} catch (e: Exception) {
Log.i("GETrequest", "was unsuccessful" + e.message)
2021-12-06 22:08:04 +02:00
throw RuntimeException()
}
}
}
} catch (e: Exception) {
// There was a problem with parameters, which means that authentication is not possible.
// In that case we will cancel the authentication immediately as it would be waste of the user's time to carry on
// before getting an inevitable error.
2021-12-04 17:08:58 +02:00
val message = MaterialAlertDialogBuilder(requireContext())
message.setTitle(getString(R.string.problem_parameters))
if (intentParams.challenge == "") {
message.setMessage(getString(R.string.problem_challenge))
} else if (intentParams.authUrl == "") {
message.setMessage(getString(R.string.problem_authurl))
} else if (intentParams.origin == "") {
message.setMessage(getString(R.string.problem_originurl))
} else {
message.setMessage(getString(R.string.problem_other))
}
message.setPositiveButton(getString(R.string.continue_button)) { _, _ ->
2021-12-04 17:08:58 +02:00
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
message.show()
}
}
/**
* Checks the state of the CAN, saved or not saved. Updates the text and logo.
*/
private fun canState() {
if (viewModel.userCan.length == 6) {
2021-12-04 17:21:07 +02:00
binding.canStatusText.text = getString(R.string.can_status_saved)
binding.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
2021-11-25 14:17:00 +02:00
canSaved = true
} else {
2021-12-04 17:21:07 +02:00
binding.canStatusText.text = getString(R.string.can_status_negative)
binding.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
2021-11-25 14:17:00 +02:00
canSaved = false
}
}
/**
* Checks the state of the PIN 1, saved or not saved. Updates the text and logo.
*/
private fun pinState() {
if (viewModel.userPin.length in 4..12) {
2021-12-04 17:21:07 +02:00
binding.pinStatusText.text = getString(R.string.pin_status_saved)
binding.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
} else {
2021-12-04 17:21:07 +02:00
binding.pinStatusText.text = getString(R.string.pin_status_negative)
binding.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
}
}
private fun getHeaders(headersString: String): Map<String, String> {
val headers = HashMap<String, String>()
val headersStringFormatted = headersString.substring(1, headersString.length - 1)
val headersJsonObject = JSONObject(headersStringFormatted)
for (name in headersJsonObject.keys()) {
headers[name] = headersJsonObject[name].toString()
}
return headers
}
2021-11-25 14:17:00 +02:00
/**
* Displays texts that inform the user whether the CAN and PIN 1 are saved on the device or not.
* This might help the user to save some time as checking menu is not necessary unless the user
* wishes to make changes to the saved CAN or PIN 1.
*/
private fun displayStates() {
canState()
pinState()
}
/**
* Method where all the initial checks that should be completed before any user input is accepted should be conducted.
*/
private fun initialChecks() {
viewModel.checkCan(requireContext())
viewModel.checkPin(requireContext())
displayStates()
}
2021-12-03 16:11:48 +02:00
/**
* Displays a help message to the user explaining what the CAN is
*/
2021-12-04 17:08:58 +02:00
private fun displayMessage(title: String, message: String) {
2021-12-03 16:11:48 +02:00
val dialog = MaterialAlertDialogBuilder(requireContext())
2021-12-04 17:08:58 +02:00
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.return_text) { _, _ -> }
2021-12-03 16:11:48 +02:00
.show()
val title = dialog.findViewById<TextView>(R.id.alertTitle)
title?.textSize = 24F
}
2021-11-25 14:17:00 +02:00
/**
* Informs user whether the ID card can be detected or not.
*/
private fun updateAction(canIsSaved: Boolean) {
if (canIsSaved) {
2021-12-04 17:21:07 +02:00
binding.detectionActionText.text = getString(R.string.action_detect)
2021-11-25 14:17:00 +02:00
enableReaderMode()
2021-12-04 17:21:07 +02:00
binding.homeActionButton.visibility = View.GONE
binding.homeHelpButton.visibility = View.GONE
2021-11-25 14:17:00 +02:00
} else {
2021-12-04 17:21:07 +02:00
binding.detectionActionText.text = getString(R.string.action_detect_unavailable)
binding.homeActionButton.text = getString(R.string.add_can_text)
binding.homeActionButton.setOnClickListener {
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(
saving = true,
fromhome = true
)
findNavController().navigate(action)
}
2021-12-04 17:21:07 +02:00
binding.homeHelpButton.setOnClickListener {
displayMessage(
getString(R.string.can_question),
getString(R.string.can_explanation)
)
2021-12-03 16:11:48 +02:00
}
2021-12-04 17:21:07 +02:00
binding.homeActionButton.visibility = View.VISIBLE
binding.homeHelpButton.visibility = View.VISIBLE
2021-11-25 14:17:00 +02:00
}
}
2021-11-25 14:17:00 +02:00
/**
* Resets the error message and allows the user to try again
*/
private fun reset() {
2021-12-04 17:21:07 +02:00
binding.homeActionButton.text = getString(R.string.try_again_text)
binding.homeActionButton.setOnClickListener {
updateAction(canSaved)
}
2021-12-04 17:21:07 +02:00
binding.homeActionButton.visibility = View.VISIBLE
2021-11-25 14:17:00 +02:00
}
/**
* Method that enables the NFC reader mode, which allows the app to communicate with the ID card and retrieve information.
*/
2021-11-25 14:17:00 +02:00
private fun enableReaderMode() {
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter == null || !adapter.isEnabled) {
2021-12-04 17:21:07 +02:00
binding.detectionActionText.text = getString(R.string.nfc_not_available)
2021-11-25 14:17:00 +02:00
} else {
adapter.enableReaderMode(activity, { tag ->
requireActivity().runOnUiThread {
2021-12-04 17:21:07 +02:00
binding.detectionActionText.text = getString(R.string.card_detected)
2021-11-25 14:17:00 +02:00
}
val card = IsoDep.get(tag)
card.timeout = 32768
card.use {
try {
val comms = Comms(it, viewModel.userCan)
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
viewModel.setUserFirstName(response[1])
viewModel.setUserLastName(response[0])
viewModel.setUserIdentificationNumber(response[2])
viewModel.setGender(response[3])
viewModel.setCitizenship(response[4])
viewModel.setExpiration(response[5])
requireActivity().runOnUiThread {
val action = HomeFragmentDirections.actionHomeFragmentToUserFragment()
findNavController().navigate(action)
}
} catch (e: Exception) {
when (e) {
is TagLostException -> requireActivity().runOnUiThread {
binding.detectionActionText.text =
getString(R.string.id_card_removed_early)
reset()
}
else -> requireActivity().runOnUiThread {
binding.detectionActionText.text =
getString(R.string.nfc_reading_error)
viewModel.deleteCan(requireContext())
canState()
reset()
}
2021-11-25 14:17:00 +02:00
}
} finally {
adapter.disableReaderMode(activity)
}
}
}, NfcAdapter.FLAG_READER_NFC_A, null)
}
}
override fun onDestroyView() {
super.onDestroyView()
2021-12-04 17:08:58 +02:00
if (receiver != null) {
requireActivity().unregisterReceiver(receiver)
}
2021-12-04 17:21:07 +02:00
_binding = null
}
}