diff --git a/app/src/main/java/com/rynkbit/jku/stuka/identity/AccessControlProfile.kt b/app/src/main/java/com/rynkbit/jku/stuka/identity/AccessControlProfile.kt new file mode 100644 index 0000000..749965f --- /dev/null +++ b/app/src/main/java/com/rynkbit/jku/stuka/identity/AccessControlProfile.kt @@ -0,0 +1,11 @@ +package com.rynkbit.jku.stuka.identity + +import androidx.security.identity.AccessControlProfile +import androidx.security.identity.AccessControlProfileId + +class AccessControlProfile { + val accessControlProfileId = AccessControlProfileId(1) + val accessControlProfile = AccessControlProfile.Builder(accessControlProfileId) + .setUserAuthenticationRequired(false) + .build() +} \ No newline at end of file diff --git a/app/src/main/java/com/rynkbit/jku/stuka/AgnosticIdentityCredentialStore.kt b/app/src/main/java/com/rynkbit/jku/stuka/identity/AgnosticIdentityCredentialStore.kt similarity index 93% rename from app/src/main/java/com/rynkbit/jku/stuka/AgnosticIdentityCredentialStore.kt rename to app/src/main/java/com/rynkbit/jku/stuka/identity/AgnosticIdentityCredentialStore.kt index d97bde7..1b1b95a 100644 --- a/app/src/main/java/com/rynkbit/jku/stuka/AgnosticIdentityCredentialStore.kt +++ b/app/src/main/java/com/rynkbit/jku/stuka/identity/AgnosticIdentityCredentialStore.kt @@ -1,4 +1,4 @@ -package com.rynkbit.jku.stuka +package com.rynkbit.jku.stuka.identity import android.content.Context import androidx.security.identity.IdentityCredentialStore diff --git a/app/src/main/java/com/rynkbit/jku/stuka/identity/StudentProfile.kt b/app/src/main/java/com/rynkbit/jku/stuka/identity/StudentProfile.kt new file mode 100644 index 0000000..780a10a --- /dev/null +++ b/app/src/main/java/com/rynkbit/jku/stuka/identity/StudentProfile.kt @@ -0,0 +1,40 @@ +package com.rynkbit.jku.stuka.identity + +import androidx.security.identity.PersonalizationData + +const val CREDENTIAL_NAMESPACE = "STUKA" +const val CREDENTIAL_STUDENT_FIRSTNAME = "FIRSTNAME" +const val CREDENTIAL_STUDENT_LASTNAME = "LASTNAME" +const val CREDENTIAL_STUDENT_BIRTHDATE = "BIRTHDATE" +const val CREDENTIAL_STUDENT_MATRICULATION_NUMBER = "MATRICULATION_NUMBER" +const val CREDENTIAL_STUDENT_STUDY_CODE = "STUDY_CODE" + +class StudentProfile( + val firstname: String = "", + val lastname: String = "", + val birthdate: String = "", + val matriculationNumber: String = "", + val studyCode: String = "", + val studentAccessControlProfile: AccessControlProfile = AccessControlProfile(), +) { + fun toPersonalizationData() = + PersonalizationData.Builder() + .addAccessControlProfile(studentAccessControlProfile.accessControlProfile) + .putEntry(CREDENTIAL_STUDENT_FIRSTNAME, firstname) + .putEntry(CREDENTIAL_STUDENT_LASTNAME, lastname) + .putEntry(CREDENTIAL_STUDENT_BIRTHDATE, birthdate) + .putEntry(CREDENTIAL_STUDENT_MATRICULATION_NUMBER, matriculationNumber) + .putEntry(CREDENTIAL_STUDENT_STUDY_CODE, studyCode) + .build() + + private fun PersonalizationData.Builder.putEntry( + key: String, + value: String + ): PersonalizationData.Builder = + this.putEntryString( + CREDENTIAL_NAMESPACE, + key, + listOf(studentAccessControlProfile.accessControlProfileId), + value + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/rynkbit/jku/stuka/identity/StudentStore.kt b/app/src/main/java/com/rynkbit/jku/stuka/identity/StudentStore.kt new file mode 100644 index 0000000..73b1f71 --- /dev/null +++ b/app/src/main/java/com/rynkbit/jku/stuka/identity/StudentStore.kt @@ -0,0 +1,101 @@ +package com.rynkbit.jku.stuka.identity + +import android.content.Context +import androidx.security.identity.IdentityCredential +import androidx.security.identity.IdentityCredentialStore +import androidx.security.identity.ResultData +import java.lang.UnsupportedOperationException + +const val CREDENTIAL_NAME = "STUDENT" +const val DOC_TYPE = "CARD" +const val IDENTITY_FEATURE = "android.hardware.identity_credential" + +class StudentStore( + context: Context, + private val agnosticIdentityCredentialStore: AgnosticIdentityCredentialStore = AgnosticIdentityCredentialStore( + context + ) +) { + + init { + + } + + fun store(student: StudentProfile) { + val credentialStore = agnosticIdentityCredentialStore.credentialStore + val credentials = credentialStore.getCredentialByName( + CREDENTIAL_NAME, + IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256 + ) + + if (credentials == null) { + createStudent(student) + } else { + updateStudent(student, credentials) + } + } + + fun get(): StudentProfile { + val credentialStore = agnosticIdentityCredentialStore.credentialStore + val credentials = credentialStore.getCredentialByName( + CREDENTIAL_NAME, + IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256 + ) + + if (credentials != null) { + val results = credentials.getEntries( + null, + mapOf( + Pair( + CREDENTIAL_NAMESPACE, listOf( + CREDENTIAL_STUDENT_FIRSTNAME, + CREDENTIAL_STUDENT_LASTNAME, + CREDENTIAL_STUDENT_BIRTHDATE, + CREDENTIAL_STUDENT_MATRICULATION_NUMBER, + CREDENTIAL_STUDENT_STUDY_CODE + ) + ) + ), + null + ) + return results.toStudent() + } + + return StudentProfile() + } + + private fun createStudent(student: StudentProfile) { + val credentialStore = agnosticIdentityCredentialStore.credentialStore + val credentials = credentialStore.createCredential(CREDENTIAL_NAME, DOC_TYPE) + credentials.personalize(student.toPersonalizationData()) + } + + private fun updateStudent(student: StudentProfile, credentials: IdentityCredential) { + val credentialStore = agnosticIdentityCredentialStore.credentialStore + + when { + credentialStore.capabilities.isUpdateSupported -> { + credentials.update(student.toPersonalizationData()) + } + credentialStore.capabilities.isDeleteSupported -> { + deleteStudent(credentials) + createStudent(student) + } + else -> { + throw UnsupportedOperationException() + } + } + } + + private fun deleteStudent(credentials: IdentityCredential) { + credentials.delete(ByteArray(1)) + } + + private fun ResultData.toStudent(): StudentProfile = StudentProfile( + getEntryString(CREDENTIAL_NAMESPACE, CREDENTIAL_STUDENT_FIRSTNAME) ?: "", + getEntryString(CREDENTIAL_NAMESPACE, CREDENTIAL_STUDENT_LASTNAME) ?: "", + getEntryString(CREDENTIAL_NAMESPACE, CREDENTIAL_STUDENT_BIRTHDATE) ?: "", + getEntryString(CREDENTIAL_NAMESPACE, CREDENTIAL_STUDENT_MATRICULATION_NUMBER) ?: "", + getEntryString(CREDENTIAL_NAMESPACE, CREDENTIAL_STUDENT_STUDY_CODE) ?: "" + ) +} diff --git a/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataFragment.kt b/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataFragment.kt index b0b686c..4b47cdb 100644 --- a/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataFragment.kt +++ b/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataFragment.kt @@ -6,15 +6,14 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText +import androidx.core.widget.addTextChangedListener import androidx.navigation.findNavController import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.snackbar.Snackbar import com.rynkbit.jku.stuka.R - -const val CREDENTIAL_NAME = "STUDENT_6" -const val DOC_TYPE = "CARD" -const val CREDENTIAL_NAMESPACE = "StuKa" -const val STUDENT_NAME = "NAME" -const val IDENTITY_FEATURE = "android.hardware.identity_credential" +import com.rynkbit.jku.stuka.identity.StudentStore +import java.lang.UnsupportedOperationException class EditDataFragment : Fragment() { private lateinit var viewModel: EditDataViewModel @@ -30,11 +29,64 @@ class EditDataFragment : Fragment() { super.onViewCreated(view, savedInstanceState) viewModel = ViewModelProvider(this).get(EditDataViewModel::class.java) + val context = requireContext() + val studentProfile = StudentStore(context).get() + + viewModel.applyStudentProfile(studentProfile) + view.apply { findViewById(R.id.fabSaveData) .setOnClickListener { + saveData() findNavController().navigate(R.id.action_editDataFragment_to_mainFragment) } + + val editFirstname = findViewById(R.id.editFirstname) + val editLastname = findViewById(R.id.editLastname) + val editBirthdate = findViewById(R.id.editBirthdate) + val editMatriculationNumber = findViewById(R.id.editMatriculationNumber) + val editStudyCode = findViewById(R.id.editStudyCode) + + editFirstname.setTextIfNotEmpty(studentProfile.firstname) + editLastname.setTextIfNotEmpty(studentProfile.lastname) + editBirthdate.setTextIfNotEmpty(studentProfile.birthdate) + editMatriculationNumber.setTextIfNotEmpty(studentProfile.matriculationNumber) + editStudyCode.setTextIfNotEmpty(studentProfile.studyCode) + + editFirstname.addTextChangedListener { + viewModel.firstname = it.toString() + } + editLastname.addTextChangedListener { + viewModel.lastname = it.toString() + } + editBirthdate.addTextChangedListener { + viewModel.birthdate = it.toString() + } + editMatriculationNumber.addTextChangedListener { + viewModel.matriculationNumber = it.toString() + } + editStudyCode.addTextChangedListener { + viewModel.studyCode = it.toString() + } } } + + private fun EditText.setTextIfNotEmpty(value: String) { + if (value.isNotEmpty()) { + setText(value) + } + } + + private fun saveData() { + val context = requireContext() + try { + StudentStore(context) + .store(viewModel.studentProfile) + } catch (e: UnsupportedOperationException) { + Snackbar + .make(requireView(), R.string.updating_credentials_not_supported, Snackbar.LENGTH_LONG) + .show() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataViewModel.kt b/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataViewModel.kt index 3c493cb..0bc8587 100644 --- a/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataViewModel.kt +++ b/app/src/main/java/com/rynkbit/jku/stuka/ui/edit/EditDataViewModel.kt @@ -1,6 +1,25 @@ package com.rynkbit.jku.stuka.ui.edit import androidx.lifecycle.ViewModel +import com.rynkbit.jku.stuka.identity.StudentProfile class EditDataViewModel : ViewModel() { + var firstname: String = "" + var lastname: String = "" + var birthdate: String = "" + var matriculationNumber: String = "" + var studyCode: String = "" + + val studentProfile + get() = StudentProfile( + firstname, lastname, birthdate, matriculationNumber, studyCode + ) + + fun applyStudentProfile(studentProfile: StudentProfile) { + firstname = studentProfile.firstname + lastname = studentProfile.lastname + birthdate = studentProfile.birthdate + matriculationNumber = studentProfile.matriculationNumber + studyCode = studentProfile.studyCode + } } \ No newline at end of file diff --git a/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainFragment.kt b/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainFragment.kt index 60ce3d9..1625d0e 100644 --- a/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainFragment.kt +++ b/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainFragment.kt @@ -2,19 +2,15 @@ package com.rynkbit.jku.stuka.ui.main import androidx.lifecycle.ViewModelProvider import android.os.Bundle -import android.util.Log import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast +import android.widget.TextView import androidx.navigation.findNavController -import androidx.security.identity.* import com.google.android.material.floatingactionbutton.FloatingActionButton -import com.google.android.material.snackbar.Snackbar -import com.rynkbit.jku.stuka.AgnosticIdentityCredentialStore import com.rynkbit.jku.stuka.R -import java.lang.Exception +import com.rynkbit.jku.stuka.identity.StudentStore class MainFragment : Fragment() { private lateinit var viewModel: MainViewModel @@ -35,50 +31,25 @@ class MainFragment : Fragment() { .setOnClickListener { findNavController().navigate(R.id.action_mainFragment_to_editDataFragment) } + + val txtFirstname = findViewById(R.id.txtFirstname) + val txtLastname = findViewById(R.id.txtLastname) + val txtBirthdate = findViewById(R.id.txtBirthdate) + val txtMatriculationNumber = findViewById(R.id.txtMatriculationNumber) + val txtStudyCode = findViewById(R.id.txtStudyCode) + + viewModel.student.observe(this@MainFragment.viewLifecycleOwner) { + txtFirstname.text = it.firstname + txtLastname.text = it.lastname + txtBirthdate.text = it.birthdate + txtMatriculationNumber.text = it.matriculationNumber + txtStudyCode.text = it.studyCode + } } -// val context = requireContext() + } -// // All of the code below was made by trial and error -// // The documentation only gives minimal insight and the error messages are close to useless -// val identityStore = AgnosticIdentityCredentialStore(context) -// val credentials = identityStore.credentialStore.createCredential(CREDENTIAL_NAME, DOC_TYPE) -// -// // For some reason an access control profile is needed, however -// // I could not find the reason why or what that does -// val accessControlProfileId = AccessControlProfileId(1) -// val accessControlProfile = AccessControlProfile.Builder(accessControlProfileId) -// .setUserAuthenticationRequired(false) -// .build() -// -// val data = PersonalizationData.Builder() -// .addAccessControlProfile(accessControlProfile) -// .putEntryString( -// CREDENTIAL_NAMESPACE, -// STUDENT_NAME, -// listOf(accessControlProfileId), -// "Test Name" -// ) -// .build() -// -// try { -// credentials.personalize(data) -// val readCredentials = identityStore.credentialStore.getCredentialByName( -// CREDENTIAL_NAME, -// IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256 -// ) -// -// // If no access control profile is created or used -// // the result data contains all keys, but a null value will be returned -// val resultData = readCredentials?.getEntries( -// null, -// mapOf(Pair(CREDENTIAL_NAMESPACE, listOf(STUDENT_NAME))), -// null -// ) -// val status = resultData?.getStatus(CREDENTIAL_NAMESPACE, STUDENT_NAME) -// val name = resultData?.getEntryString(CREDENTIAL_NAMESPACE, STUDENT_NAME) -// Snackbar.make(view, "Somehow this worked: $status: $name", Snackbar.LENGTH_SHORT).show() -// } catch (e: Exception) { -// Log.e(MainFragment::class.java.simpleName, e.message, e) -// } + override fun onResume() { + super.onResume() + viewModel.student.postValue(StudentStore(requireContext()).get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainViewModel.kt b/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainViewModel.kt index 8756662..54dbf9d 100644 --- a/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/rynkbit/jku/stuka/ui/main/MainViewModel.kt @@ -1,6 +1,9 @@ package com.rynkbit.jku.stuka.ui.main +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.rynkbit.jku.stuka.identity.StudentProfile class MainViewModel : ViewModel() { + var student = MutableLiveData() } \ No newline at end of file diff --git a/app/src/main/res/layout/edit_data_fragment.xml b/app/src/main/res/layout/edit_data_fragment.xml index e069712..89d135b 100644 --- a/app/src/main/res/layout/edit_data_fragment.xml +++ b/app/src/main/res/layout/edit_data_fragment.xml @@ -30,11 +30,10 @@ app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/editFirstname" /> + app:layout_constraintTop_toBottomOf="@+id/editLastname" /> + app:layout_constraintTop_toBottomOf="@+id/editBirthdate" /> + app:layout_constraintTop_toBottomOf="@+id/editMatriculationNumber" /> Study code Unset Edit data + Your device does not support updating credentials \ No newline at end of file