diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index 6ec4800..80332d7 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -92,6 +92,7 @@ class HomeActivity : AppCompatActivity() { R.id.nav_contacts -> show(ContactsFragment()) R.id.nav_transfer_history -> show(TransferHistoryFragment()) R.id.nav_finances -> show(FinancingFragment()) + R.id.nav_otp -> show(OtpFragment()) R.id.nav_settings -> show(SettingsFragment()) else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt new file mode 100644 index 0000000..ced7279 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt @@ -0,0 +1,91 @@ +package sh.sar.basedbank.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import sh.sar.basedbank.databinding.FragmentOtpBinding +import sh.sar.basedbank.databinding.ItemOtpCardBinding +import sh.sar.basedbank.util.CredentialStore +import sh.sar.basedbank.util.Totp + +class OtpFragment : Fragment() { + + private var _binding: FragmentOtpBinding? = null + private val binding get() = _binding!! + + private data class OtpEntry(val label: String, val seed: String) + + private inner class OtpAdapter(private val entries: List) : + RecyclerView.Adapter() { + + inner class VH(val b: ItemOtpCardBinding) : RecyclerView.ViewHolder(b.root) + + override fun getItemCount() = entries.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + VH(ItemOtpCardBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + + override fun onBindViewHolder(holder: VH, position: Int) { + holder.b.tvOtpLabel.text = entries[position].label + update(holder.b, entries[position].seed) + } + + fun tick() { + for (i in entries.indices) { + val vh = (binding.recyclerView.findViewHolderForAdapterPosition(i) as? VH) ?: continue + update(vh.b, entries[i].seed) + } + } + + private fun update(b: ItemOtpCardBinding, seed: String) { + val epochSeconds = System.currentTimeMillis() / 1000L + val secondsInPeriod = (epochSeconds % 30).toInt() + val remaining = 30 - secondsInPeriod + val code = try { Totp.generate(seed) } catch (_: Exception) { "------" } + b.tvOtpCode.text = "${code.take(3)} ${code.drop(3)}" + b.otpProgress.progress = remaining + b.tvOtpCountdown.text = "Refreshes in $remaining second${if (remaining == 1) "" else "s"}" + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentOtpBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val store = CredentialStore(requireContext()) + val entries = mutableListOf() + store.loadMibCredentials()?.let { entries.add(OtpEntry("MIB · ${it.username}", it.otpSeed)) } + store.loadBmlCredentials()?.let { entries.add(OtpEntry("BML · ${it.username}", it.otpSeed)) } + + val adapter = OtpAdapter(entries) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + + viewLifecycleOwner.lifecycleScope.launch { + while (isActive) { + adapter.tick() + delay(1_000) + } + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = "OTP Codes" + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/res/layout/fragment_otp.xml b/app/src/main/res/layout/fragment_otp.xml new file mode 100644 index 0000000..5b92774 --- /dev/null +++ b/app/src/main/res/layout/fragment_otp.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/layout/item_otp_card.xml b/app/src/main/res/layout/item_otp_card.xml new file mode 100644 index 0000000..37566f6 --- /dev/null +++ b/app/src/main/res/layout/item_otp_card.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml index 84fb545..2e9563f 100644 --- a/app/src/main/res/menu/drawer_menu.xml +++ b/app/src/main/res/menu/drawer_menu.xml @@ -32,6 +32,9 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db2bc93..2e3a45d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,7 @@ Transfer History Finances Card Settings + OTP Codes Settings Open navigation Close navigation