diff --git a/MobileAuthApp/app/build.gradle b/MobileAuthApp/app/build.gradle index bc4a034..38896fb 100644 --- a/MobileAuthApp/app/build.gradle +++ b/MobileAuthApp/app/build.gradle @@ -68,8 +68,4 @@ dependencies { 'io.jsonwebtoken:jjwt-gson:0.11.2' implementation 'com.koushikdutta.ion:ion:3.1.0' - - // Retrofit + Moshi Converter - implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' - implementation 'com.squareup.moshi:moshi-kotlin:1.9.3' } \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt index bff62f4..938c9ef 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.nfc.NfcAdapter +import android.nfc.TagLostException import android.nfc.tech.IsoDep import android.os.Bundle import android.os.CountDownTimer @@ -33,7 +34,7 @@ class AuthFragment : Fragment() { private val viewModel: SmartCardViewModel by activityViewModels() - private val intentParameters: ParametersViewModel by activityViewModels() + private val paramsModel: ParametersViewModel by activityViewModels() private var binding: FragmentAuthBinding? = null @@ -66,15 +67,37 @@ class AuthFragment : Fragment() { override fun onFinish() { Thread.sleep(750) - goToTheStart() + cancelAuth() } }.start() - //binding!!.nextButton.visibility = View.INVISIBLE + // The button exists in code for testing reasons, but not visible to the user anymore unless visibility is changed in the code. + binding!!.nextButton.visibility = View.GONE binding!!.nextButton.setOnClickListener { goToNextFragment() } - binding!!.cancelButton.setOnClickListener { goToTheStart() } + binding!!.cancelButton.setOnClickListener { cancelAuth() } val adapter = NfcAdapter.getDefaultAdapter(activity) if (adapter != null) getInfoFromIdCard(adapter) + else { // If NFC adapter can not be detected then end the auth process as it is not possible to read an ID card + cancelAuth() // It would be a good idea to show user some notification as it might be confusing if the app suddenly closes + } + } + + private fun goToNextFragment() { + timer.cancel() + val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile) + findNavController().navigate(action) + } + + private fun cancelAuth() { + viewModel.clearUserInfo() + timer.cancel() + if (args.mobile) { + val resultIntent = Intent() + requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) + requireActivity().finish() + } else { + requireActivity().finishAndRemoveTask() + } } private fun getInfoFromIdCard(adapter: NfcAdapter) { @@ -88,34 +111,35 @@ class AuthFragment : Fragment() { card.use { try { val comms = Comms(it, viewModel.userCan) - if (args.auth) { - val jws = Authenticator(comms).authenticate( - intentParameters.challenge, - intentParameters.origin, - viewModel.userPin - ) - intentParameters.setToken(jws) - } else { - 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]) - } + val jws = Authenticator(comms).authenticate( + paramsModel.challenge, + paramsModel.origin, + viewModel.userPin + ) + paramsModel.setToken(jws) requireActivity().runOnUiThread { - binding!!.timeCounter.text = getString(R.string.data_read) + goToNextFragment() } } catch (e: Exception) { - requireActivity().runOnUiThread { - binding!!.timeCounter.text = getString(R.string.no_success) + when(e) { + is TagLostException -> requireActivity().runOnUiThread { binding!!.timeCounter.text = getString(R.string.id_card_removed_early) } + else -> { + when ("invalid pin") { + in e.message.toString().lowercase() -> requireActivity().runOnUiThread { + val messagePieces = e.message.toString().split(" ") + binding!!.timeCounter.text = getString(R.string.wrong_pin, messagePieces[messagePieces.size - 1]) + viewModel.deletePin(requireContext()) + } + else -> requireActivity().runOnUiThread { + binding!!.timeCounter.text = getString(R.string.wrong_can_text) + viewModel.deleteCan(requireContext()) + } + } + } } - // If the CAN is wrong we will also delete the saved CAN so that the user won't use it again. - viewModel.deleteCan(requireContext()) - // Gives user some time to read the error message - Thread.sleep(1000) - goToTheStart() + // Give user some time to read the error message + Thread.sleep(2000) + cancelAuth() } finally { adapter.disableReaderMode(activity) } @@ -123,33 +147,6 @@ class AuthFragment : Fragment() { }, NfcAdapter.FLAG_READER_NFC_A, null) } - private fun goToNextFragment() { - timer.cancel() - if (args.auth) { - val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile) - findNavController().navigate(action) - } else { - findNavController().navigate(R.id.action_authFragment_to_userFragment) - } - } - - private fun goToTheStart() { - viewModel.clearUserInfo() - timer.cancel() - if (args.reading) { - findNavController().navigate(R.id.action_authFragment_to_homeFragment) - } else { - if (!args.mobile) { - //Currently for some reason the activity is not killed entirely. Must be looked into further. - requireActivity().finishAndRemoveTask() - } else { - val resultIntent = Intent() - requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) - requireActivity().finish() - } - } - } - override fun onDestroy() { super.onDestroy() binding = null diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt index 1b281aa..2b4806f 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt @@ -3,17 +3,22 @@ package com.tarkvaraprojekt.mobileauthapp import android.app.AlertDialog import android.content.Intent import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel +import org.w3c.dom.Text /** * Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved @@ -28,8 +33,6 @@ class CanFragment : Fragment() { // Navigation arguments: // saving = true means that we are navigating here from the settings menu and must return to the settings menu. - // reading = true means that we are only reading the information from the ID card that does not need PIN 1, - // this information is passed on to the next PinFragment. private val args: CanFragmentArgs by navArgs() override fun onCreateView( @@ -44,13 +47,10 @@ class CanFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) checkIfSkip() - // If the user arrives from the settings menu then the button should say - // save instead of continue. - if (args.saving) { - binding!!.nextButton.text = getString(R.string.save_text) + binding!!.canTextField.editText?.addTextChangedListener { + checkEnteredCan() } - binding!!.nextButton.setOnClickListener { checkEnteredCan() } - binding!!.cancelButton.setOnClickListener { goToTheStart() } + binding!!.buttonCancel.setOnClickListener { goToTheStart() } } /** @@ -67,66 +67,22 @@ class CanFragment : Fragment() { * Takes user to the next fragment, which is PinFragment. */ private fun goToTheNextFragment() { - val action = CanFragmentDirections.actionCanFragmentToPinFragment(reading = args.reading, auth = args.auth, mobile = args.mobile) + val action = CanFragmentDirections.actionCanFragmentToPinFragment(auth = args.auth, mobile = args.mobile) findNavController().navigate(action) } - /** - * Checks whether the user has entered a 6 digit can to the input field. - * If yes then the user is allowed to continue otherwise the user is - * allowed to modify the entered can. - */ - private fun checkEnteredCan() { - val enteredCan = binding!!.canEditText.editText?.text.toString() - if (enteredCan.length == 6) { - viewModel.setUserCan(enteredCan) - if (args.saving) { - viewModel.storeCan(requireContext()) - goToTheStart() - } else { - val storeCanQuestion = getDialog() - storeCanQuestion?.show() - } - } else { - Toast.makeText(requireContext(), getString(R.string.length_can), Toast.LENGTH_SHORT) - .show() - } - } - - /** - * Builds a dialog that asks the user whether the entered CAN should be saved - * on the device or not. - */ - private fun getDialog(): AlertDialog? { - return activity?.let { frag -> - val builder = AlertDialog.Builder(frag) - builder.apply { - // If response is positive then save the CAN on the device. - setPositiveButton(R.string.save_text) { _, _ -> - viewModel.storeCan( - requireContext() - ) - goToTheNextFragment() - } - setNegativeButton(R.string.deny_text) { _, _ -> - goToTheNextFragment() - } - } - builder.setMessage(R.string.can_save_request) - builder.setTitle(R.string.save_can_title) - builder.create() - } - } - /** * Navigates the user back to the start depending on where the user arrived. * If the user arrived from the settings menu then the start is the settings menu * not the HomeFragment. */ private fun goToTheStart() { - // TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment. if (args.saving) { - findNavController().navigate(R.id.action_canFragment_to_settingsFragment) + if (args.fromhome) { + findNavController().navigate(R.id.action_canFragment_to_homeFragment) + } else { + findNavController().navigate(R.id.action_canFragment_to_settingsFragment) + } } else if (args.auth || args.mobile) { if (args.mobile) { val resultIntent = Intent() @@ -140,6 +96,35 @@ class CanFragment : Fragment() { } } + /** + * Method that creates and shows a snackbar that tells the user that CAN has been saved + */ + private fun showSnackbar() { + val snackbar = Snackbar.make(requireView(), R.string.can_status_saved, Snackbar.LENGTH_SHORT) + val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text) + snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text)) + snackbar.show() + } + + /** + * Checks whether the user has entered a 6 digit can to the input field. + * If yes then the user is allowed to continue otherwise the user is + * allowed to modify the entered can. + */ + private fun checkEnteredCan() { + val enteredCan = binding!!.canTextField.editText?.text.toString() + if (enteredCan.length == 6) { + viewModel.setUserCan(enteredCan) + viewModel.storeCan(requireContext()) //Maybe storeCan should always automatically call setUserCan method as well because these methods usually are used together + showSnackbar() + if (args.saving) { + goToTheStart() + } else { + goToTheNextFragment() + } + } + } + override fun onDestroy() { super.onDestroy() binding = null diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt index e1888f4..659d131 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt @@ -1,20 +1,30 @@ package com.tarkvaraprojekt.mobileauthapp +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +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 +import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +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 java.lang.Exception -import java.net.URLDecoder /** * HomeFragment is only shown to the user when then the user launches the application. When the application @@ -31,6 +41,13 @@ class HomeFragment : Fragment() { private var binding: FragmentHomeBinding? = null + // 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, @@ -46,73 +63,63 @@ class HomeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initialChecks() - var auth = false if (requireActivity().intent.data?.getQueryParameter("action") != null) { // Currently we only support authentication not signing. auth = true } val mobile = requireActivity().intent.getBooleanExtra("mobile", false) - if (auth || mobile){ - try { - if (mobile) { - // We use !! because we want an exception when something is not right. - intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!) - intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!) - intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!) - } else { //Website - 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")!!) + if (auth || mobile) { + startAuthentication(mobile) + } else { + receiver = object : BroadcastReceiver() { + override fun onReceive(p0: Context?, p1: Intent?) { + updateAction(canSaved) } - } catch (e: Exception) { - // There was a problem with parameters, which means that authentication is not possible. - val resultIntent = Intent() - requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) - requireActivity().finish() } - goToTheNextFragment(true, mobile) + val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED) + requireActivity().registerReceiver(receiver, filter) + updateAction(canSaved) } - binding!!.beginButton.setOnClickListener { goToTheNextFragment() } - } - - /** - * Method where all the initial checks that should be done before any user input is accepted should be added. - */ - private fun initialChecks() { - viewModel.checkCan(requireContext()) - viewModel.checkPin(requireContext()) - displayStates() } /** * Starts the process of interacting with the ID card by sending user to the CAN fragment. */ - private fun goToTheNextFragment(auth: Boolean = false, mobile: Boolean = false) { - // Making settings menu inactive + private fun goToTheNextFragment(mobile: Boolean = false) { (activity as MainActivity).menuAvailable = false - // Currently saving is true because the application is not yet integrated with - // other applications or websites. - // TODO: Check the navigation action default values. Not everything has to be declared explicitly. - if (auth) { - val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(reading = false, auth = true, mobile = mobile) - findNavController().navigate(action) - } else { - val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(reading = true, auth = false, mobile = mobile) - findNavController().navigate(action) - } + val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile) + findNavController().navigate(action) } /** - * 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. + * Method that starts the authentication use case. + * + * NOTE: Comment out try-catch block when testing without backend */ - private fun displayStates() { - canState() - pinState() + 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. + intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!) + intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!) + intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!) + } else { //Website + 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")!!) + } + } 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. + val resultIntent = Intent() + requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) + requireActivity().finish() + } + goToTheNextFragment(mobile) } /** @@ -122,9 +129,11 @@ class HomeFragment : Fragment() { if (viewModel.userCan.length == 6) { binding!!.canStatusText.text = getString(R.string.can_status_saved) binding!!.canStatusLogo.setImageResource(R.drawable.ic_check_logo) + canSaved = true } else { binding!!.canStatusText.text = getString(R.string.can_status_negative) binding!!.canStatusLogo.setImageResource(R.drawable.ic_info_logo) + canSaved = false } } @@ -141,8 +150,126 @@ class HomeFragment : Fragment() { } } + /** + * 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() + } + + /** + * Displays a help message to the user explaining what the CAN is + */ + private fun displayMessage() { + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.can_question)) + .setMessage(getString(R.string.can_explanation)) + .setPositiveButton(R.string.return_text){_, _ -> } + .show() + val title = dialog.findViewById(R.id.alertTitle) + title?.textSize = 24F + + } + + /** + * Informs user whether the ID card can be detected or not. + */ + private fun updateAction(canIsSaved: Boolean) { + if (canIsSaved) { + binding!!.detectionActionText.text = getString(R.string.action_detect) + enableReaderMode() + binding!!.homeActionButton.visibility = View.GONE + binding!!.homeHelpButton.visibility = View.GONE + } else { + 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) + } + binding!!.homeHelpButton.setOnClickListener { + displayMessage() + } + binding!!.homeActionButton.visibility = View.VISIBLE + binding!!.homeHelpButton.visibility = View.VISIBLE + } + } + + /** + * Resets the error message and allows the user to try again + */ + private fun reset() { + binding!!.homeActionButton.text = getString(R.string.try_again_text) + binding!!.homeActionButton.setOnClickListener { + updateAction(canSaved) + } + binding!!.homeActionButton.visibility = View.VISIBLE + } + + /** + * Method that enables the NFC reader mode, which allows the app to communicate with the ID card and retrieve information. + */ + private fun enableReaderMode() { + val adapter = NfcAdapter.getDefaultAdapter(activity) + if (adapter == null || !adapter.isEnabled) { + binding!!.detectionActionText.text = getString(R.string.nfc_not_available) + } else { + adapter.enableReaderMode(activity, { tag -> + requireActivity().runOnUiThread { + binding!!.detectionActionText.text = getString(R.string.card_detected) + } + 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() + } + } + } finally { + adapter.disableReaderMode(activity) + } + } + }, NfcAdapter.FLAG_READER_NFC_A, null) + } + } + override fun onDestroyView() { super.onDestroyView() + requireActivity().unregisterReceiver(receiver) binding = null } } \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/MainActivity.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/MainActivity.kt index 638d88e..8ec228c 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/MainActivity.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/MainActivity.kt @@ -20,6 +20,8 @@ class MainActivity : AppCompatActivity() { // If true the settings menu can be accessed from the toolbar in the upper part of the screen. var menuAvailable: Boolean = true + var inMenu: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityMainBinding.inflate(layoutInflater) @@ -40,9 +42,13 @@ class MainActivity : AppCompatActivity() { R.id.menu_settings_option -> { if (menuAvailable) { navigationController.navigate(R.id.action_homeFragment_to_settingsFragment) + menuAvailable = false + inMenu = true true } else { - Toast.makeText(this, getString(R.string.unavailable), Toast.LENGTH_SHORT).show() + if (!inMenu) { + Toast.makeText(this, getString(R.string.menu_unavailable_message), Toast.LENGTH_SHORT).show() + } false } } diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt index 86dbbc1..2ed1138 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt @@ -1,17 +1,22 @@ package com.tarkvaraprojekt.mobileauthapp import android.app.AlertDialog +import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel @@ -28,10 +33,10 @@ class PinFragment : Fragment() { // Navigation arguments: // saving = true means that the user must be returned to the settings menu - // reading = true means that we are reading information from the ID card that does - // not require PIN 1 so it is not necessary to ask it. private val args: PinFragmentArgs by navArgs() + private var saveToggle = true + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -44,90 +49,47 @@ class PinFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) checkIfSkip() - // If the user arrives from the settings menu then the button says - // save instead of continue. + // Switch should be not visible when user is in savings mode if (args.saving) { - binding!!.nextButton.text = getString(R.string.save_text) - } - binding!!.nextButton.setOnClickListener { checkEnteredPin() } - binding!!.cancelButton.setOnClickListener { goToTheStart() } - } - - /** - * Checks if the current fragment can be skipped or not. - * If the user has PIN 1 saved on the device or PIN 1 is not required - * then the PIN 1 won't be asked. - */ - private fun checkIfSkip() { - if (args.reading) { - goToTheNextFragment() - } else if (viewModel.userPin.length in 4..12) { - goToTheNextFragment() + binding!!.savePinQuestion.visibility = View.GONE + binding!!.saveLayout.visibility = View.GONE + } else { + saveToggle = + activity?.getPreferences(Context.MODE_PRIVATE)?.getBoolean("saveToggle", true) == true //Android Studio recommendation to get rid of Boolean?. + Log.i("myLogging", activity?.getPreferences(Context.MODE_PRIVATE)?.getBoolean("saveToggle", true).toString()) + if (!saveToggle) { + binding!!.saveSwitch.isChecked = false + } + binding!!.saveSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + binding!!.saveStatus.text = getString(R.string.pin_save_on) + activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", true)?.apply() + } else { + binding!!.saveStatus.text = getString(R.string.pin_save_off) + activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", false)?.apply() + } + saveToggle = !saveToggle + } } + binding!!.buttonContinue.setOnClickListener { checkEnteredPin() } + binding!!.buttonCancel.setOnClickListener { goToTheStart() } } /** * Takes user to the next fragment, which is AuthFragment. */ private fun goToTheNextFragment() { - val action = PinFragmentDirections.actionPinFragmentToAuthFragment(reading = args.reading, auth = args.auth, mobile = args.mobile) + val action = PinFragmentDirections.actionPinFragmentToAuthFragment(auth = args.auth, mobile = args.mobile) findNavController().navigate(action) } - /** - * Checks whether the user has entered a PIN 1 with length between [4, 12] in the - * input field. If yes then the user is allowed to continue otherwise the user is - * allowed to modify the entered PIN 1. - */ - private fun checkEnteredPin() { - val enteredPin = binding!!.pinEditText.editText?.text.toString() - if (enteredPin.length in 4..12) { - viewModel.setUserPin(enteredPin) - if (args.saving) { - viewModel.storePin(requireContext()) - goToTheStart() - } else { - val storePinQuestion = getDialog() - storePinQuestion?.show() - } - } else { - Toast.makeText(requireContext(), getString(R.string.length_pin), Toast.LENGTH_SHORT) - .show() - } - } - - /** - * Builds a dialog that asks the user whether the entered PIN 1 should be saved - * on the device or not. - */ - private fun getDialog(): AlertDialog? { - return activity?.let { frag -> - val builder = AlertDialog.Builder(frag) - builder.apply { - // If response is positive save the PIN 1 on the device. - setPositiveButton(R.string.save_text) { _, _ -> - viewModel.storePin( - requireContext() - ) - goToTheNextFragment() - } - setNegativeButton(R.string.deny_text) { _, _ -> - goToTheNextFragment() - } - } - builder.setMessage(R.string.pin_save_request) - builder.setTitle(R.string.save_pin_title) - builder.create() - } - } - /** * Returns user to the start. If the user arrived from the settings menu then the start is * settings menu not the HomeFragment. */ private fun goToTheStart() { if (args.saving) { - findNavController().navigate(R.id.action_canFragment_to_settingsFragment) + findNavController().navigate(R.id.action_pinFragment_to_settingsFragment) } else if (args.auth || args.mobile) { if (args.mobile) { val resultIntent = Intent() @@ -141,6 +103,53 @@ class PinFragment : Fragment() { } } + /** + * Checks if the current fragment can be skipped or not. + * If the user has PIN 1 saved on the device or PIN 1 is not required + * then the PIN 1 won't be asked. + */ + private fun checkIfSkip() { + if (viewModel.userPin.length in 4..12) { + goToTheNextFragment() + } + } + + /** + * Method that creates and shows a snackbar that tells the user that PIN 1 has been saved + */ + private fun showSnackbar() { + val snackbar = Snackbar.make(requireView(), R.string.pin_status_saved, Snackbar.LENGTH_SHORT) + val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text) + snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text)) + snackbar.show() + } + + /** + * Checks whether the user has entered a PIN 1 with length between [4, 12] in the + * input field. If yes then the user is allowed to continue otherwise the user is + * allowed to modify the entered PIN 1. + */ + private fun checkEnteredPin() { + val enteredPin = binding!!.pinTextField.editText?.text.toString() + if (enteredPin.length in 4..12) { + viewModel.setUserPin(enteredPin) + if (args.saving) { + viewModel.storePin(requireContext()) + showSnackbar() + goToTheStart() + } else { + if (saveToggle) { + viewModel.storePin(requireContext()) + showSnackbar() + } + goToTheNextFragment() + } + } else { + Toast.makeText(requireContext(), getString(R.string.pin_helper_text), Toast.LENGTH_SHORT) + .show() + } + } + override fun onDestroy() { super.onDestroy() binding = null diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt index e658c72..b397820 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt @@ -39,10 +39,21 @@ class ResultFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding!!.resultBackButton.visibility = View.GONE postToken() } + /** + * Only used when the MobileAuthApp was launched by an app. Not for website use. + */ + private fun createResponse(success: Boolean = true, result: String = "noResult", token: String = "noToken") { + val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED + val resultIntent = Intent() + resultIntent.putExtra("result", result) + resultIntent.putExtra("token", token) + requireActivity().setResult(responseCode, resultIntent) + requireActivity().finish() + } + /** * Makes a POST request to the backend server with a tokenItem */ @@ -57,17 +68,13 @@ class ResultFragment : Fragment() { .setJsonObjectBody(json) .asJsonObject() .setCallback { e, result -> - // do stuff with the result or error if (result == null) { - // TODO: Set auth message failed and close the app - Log.i("Log thingy fail", "result was null") if (args.mobile) { createResponse(false) } else { requireActivity().finishAndRemoveTask() } } else { - Log.i("POST request response", result.toString()) if (args.mobile) { createResponse(true, result.toString(), paramsModel.token) } else { @@ -77,18 +84,6 @@ class ResultFragment : Fragment() { } } - /** - * Only used when the MobileAuthApp was launched by an app. Not for website use. - */ - private fun createResponse(success: Boolean = true, result: String = "noResult", token: String = "noToken") { - val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED - val resultIntent = Intent() - resultIntent.putExtra("result", result) - resultIntent.putExtra("token", token) - requireActivity().setResult(responseCode, resultIntent) - requireActivity().finish() - } - override fun onDestroy() { super.onDestroy() binding = null diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/menu/SettingsFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/menu/SettingsFragment.kt index 87e822c..d968fb5 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/menu/SettingsFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/menu/SettingsFragment.kt @@ -1,13 +1,17 @@ package com.tarkvaraprojekt.mobileauthapp.menu import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar +import com.tarkvaraprojekt.mobileauthapp.MainActivity import com.tarkvaraprojekt.mobileauthapp.R import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentSettingsBinding import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel @@ -45,6 +49,16 @@ class SettingsFragment : Fragment() { binding!!.returnButton.setOnClickListener { backToHome() } } + /** + * Method for showing a snackbar with a message that is given as a parameter + */ + private fun showSnackbar(message: String) { + val snackbar = Snackbar.make(requireView(), message, Snackbar.LENGTH_SHORT) + val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text) + snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text)) + snackbar.show() + } + /** * Method for showing the CAN field to the user and can be used to refresh the field as well. */ @@ -54,7 +68,7 @@ class SettingsFragment : Fragment() { binding!!.canMenuAction.text = getString(R.string.can_delete) } else { binding!!.canSaved.text = getString(R.string.saved_can, getString(R.string.missing)) - binding!!.canMenuAction.text = getString(R.string.can_add) + binding!!.canMenuAction.text = getString(R.string.add_can_text) } } @@ -66,7 +80,9 @@ class SettingsFragment : Fragment() { if (viewModel.userCan.length == 6) { viewModel.deleteCan(requireContext()) showCanField() + showSnackbar(getString(R.string.can_deleted)) } else { + (activity as MainActivity).inMenu = false val action = SettingsFragmentDirections.actionSettingsFragmentToCanFragment(saving = true) findNavController().navigate(action) } @@ -100,7 +116,9 @@ class SettingsFragment : Fragment() { if (viewModel.userPin.length in 4..12) { viewModel.deletePin(requireContext()) showPinField() + showSnackbar(getString(R.string.pin_deleted)) } else { + (activity as MainActivity).inMenu = false val action = SettingsFragmentDirections.actionSettingsFragmentToPinFragment(saving = true) findNavController().navigate(action) } @@ -130,6 +148,7 @@ class SettingsFragment : Fragment() { * Navigates back to home fragment. */ private fun backToHome() { + (activity as MainActivity).inMenu = false findNavController().navigate(R.id.action_settingsFragment_to_homeFragment) } diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt deleted file mode 100644 index 67b952b..0000000 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tarkvaraprojekt.mobileauthapp.network - -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Headers -import retrofit2.http.POST - -/** - * Class for making HTTP requests - * Based on https://developer.android.com/courses/pathways/android-basics-kotlin-unit-4-pathway-2 - */ -const val BASE_URL = - "https://6bb0-85-253-195-252.ngrok.io" - -private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() -private val retrofit = Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(BASE_URL).build() - -interface TokenApiService { - @Headers("Content-Type: application/json") - @POST("/auth/authentication") - suspend fun postToken(@Body data: String): Response -} - -object TokenApi { - val retrofitService : TokenApiService by lazy { - retrofit.create(TokenApiService::class.java) - } -} \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenItem.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenItem.kt deleted file mode 100644 index b7bb0c1..0000000 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenItem.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tarkvaraprojekt.mobileauthapp.network - -/** - * TokenItem for making POST request. - */ -data class TokenItem ( - val token: String, - val challenge: String, -) \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/res/drawable/ic_launcher_background.xml b/MobileAuthApp/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..ca78d5c 100644 --- a/MobileAuthApp/app/src/main/res/drawable/ic_launcher_background.xml +++ b/MobileAuthApp/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,7 +5,7 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:padding="@dimen/padding_small"> + android:textSize="@dimen/regular_text" /> @@ -60,9 +61,9 @@ android:id="@+id/next_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="24dp" - android:text="@string/next_text" - android:textSize="15sp" + android:layout_marginTop="@dimen/margin_big" + android:text="@string/continue_button" + android:textSize="@dimen/regular_text" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/cancel_button" app:layout_constraintTop_toBottomOf="@id/card_view" /> @@ -71,10 +72,10 @@ android:id="@+id/cancel_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="24dp" + android:layout_marginTop="@dimen/margin_big" + android:layout_marginStart="@dimen/padding_tiny" android:text="@string/cancel_text" - android:textSize="15sp" - app:layout_constraintEnd_toStartOf="@id/next_button" + android:textSize="@dimen/regular_text" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/card_view" /> diff --git a/MobileAuthApp/app/src/main/res/layout/fragment_can.xml b/MobileAuthApp/app/src/main/res/layout/fragment_can.xml index f7bcd1e..c4cff63 100644 --- a/MobileAuthApp/app/src/main/res/layout/fragment_can.xml +++ b/MobileAuthApp/app/src/main/res/layout/fragment_can.xml @@ -4,82 +4,57 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="24dp" - tools:context=".CanFragment"> + android:padding="@dimen/padding" + tools:context=".MainActivity"> - + app:layout_constraintTop_toTopOf="parent"/> - + + + android:textSize="@dimen/regular_text" + android:fontFamily="sans-serif" + android:inputType="number" + android:singleLine="true" + /> - - - - - - - - - - - +