Working on Encryption

This commit is contained in:
Yogesh Paliyal
2021-02-07 20:52:52 +05:30
parent 36caa3a5d1
commit ca001ad398
12 changed files with 446 additions and 129 deletions

View File

@@ -6,6 +6,8 @@ plugins {
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
@@ -38,12 +40,20 @@ android {
viewBinding = true
dataBinding = true
}
flavorDimensions "default"
productFlavors {
production {
}
staging {
applicationIdSuffix ".staging"
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
@@ -69,10 +79,15 @@ dependencies {
implementation "androidx.room:room-ktx:$room_version"
implementation 'com.github.yogeshpaliyal:Android-Universal-Recycler-View-Adapter:1.0.1'
implementation 'com.github.yogeshpaliyal:Android-Universal-Recycler-View-Adapter:1.0.2'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.preference:preference-ktx:1.1.1"
//Androidx Security
implementation "androidx.security:security-crypto:1.1.0-alpha03"
}

View File

@@ -26,6 +26,8 @@
<activity
android:name=".ui.nav.DashboardActivity"
android:windowSoftInputMode="adjustPan"/>
<activity android:name=".ui.detail.DetailActivity"
android:windowSoftInputMode="adjustResize"/>
<meta-data
android:name="preloaded_fonts"

View File

@@ -0,0 +1,14 @@
package com.yogeshpaliyal.keypass.db_helper
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 07-02-2021 18:52
*/
class CryptoException : Exception {
constructor() {}
constructor(message: String?, throwable: Throwable?) : super(message, throwable) {}
}

View File

@@ -0,0 +1,232 @@
package com.yogeshpaliyal.keypass.db_helper
import com.yogeshpaliyal.universal_adapter.utils.LogHelper
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.security.InvalidKeyException
import java.security.Key
import java.security.NoSuchAlgorithmException
import javax.crypto.*
import javax.crypto.spec.SecretKeySpec
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 07-02-2021 18:50
*/
object EncryptionHelper {
private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES"
// private const val TRANSFORMATION = "DES/CBC/PKCS5Padding"
@Throws(CryptoException::class)
fun encrypt(key: String?, inputFile: File?, outputFile: File?) {
LogHelper.logD("FindingBug", "Key => ${key} \n inputFile => ${inputFile?.path} \n Output File => ${outputFile?.path}")
doCryptoEncrypt(Cipher.ENCRYPT_MODE, key!!, inputFile!!, outputFile!!)
}
@Throws(CryptoException::class)
fun decrypt(
key: String?,
inputFile: File?,
outputFile: File?
) {
doCryptoDecrypt(Cipher.DECRYPT_MODE, key!!, inputFile!!, outputFile!!)
}
/*@Throws(CryptoException::class)
private fun doCrypto(
cipherMode: Int, key: String, inputFile: File,
outputFile: File
) {
try {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(cipherMode, secretKey)
val inputStream = FileInputStream(inputFile)
val inputBytes = inputFile.readBytes()
val outputBytes = cipher.doFinal(inputBytes)
val outputStream = FileOutputStream(outputFile)
outputStream.write(outputBytes)
inputStream.close()
outputStream.close()
} catch (ex: NoSuchPaddingException) {
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: NoSuchAlgorithmException) {
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: InvalidKeyException) {
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: BadPaddingException) {
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IllegalBlockSizeException) {
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IOException) {
throw CryptoException("Error encrypting/decrypting file", ex)
}
}*/
@Throws(CryptoException::class)
private fun doCrypto(
cipherMode: Int, key: String, inputFile: File,
outputFile: File
) {
try {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(cipherMode, secretKey)
val inputStream = FileInputStream(inputFile)
val outputStream = FileOutputStream(outputFile)
val cis = CipherInputStream(inputStream, cipher)
var numberOfBytedRead: Int = 0
val buffer = ByteArray(4096)
while (numberOfBytedRead >= 0) {
outputStream.write(buffer, 0, numberOfBytedRead);
numberOfBytedRead = cis.read(buffer)
}
inputStream.close()
outputStream.close()
} catch (ex: NoSuchPaddingException) {
//Log.d("TestingEnc","NoSuchPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: NoSuchAlgorithmException) {
//Log.d("TestingEnc","NoSuchAlgorithmException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: InvalidKeyException) {
// Log.d("TestingEnc","InvalidKeyException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: BadPaddingException) {
// Log.d("TestingEnc","BadPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IllegalBlockSizeException) {
// Log.d("TestingEnc","IllegalBlockSizeException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IOException) {
// Log.d("TestingEnc","IOException")
throw CryptoException("Error encrypting/decrypting file", ex)
}
}
@Throws(CryptoException::class)
private fun doCryptoEncrypt(
cipherMode: Int, key: String, inputFile: File,
outputFile: File
) {
try {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(cipherMode, secretKey)
// val inputStream = FileInputStream(inputFile)
// val outputStream = FileOutputStream(outputFile)
FileInputStream(inputFile).use {
val inputStream = it
FileOutputStream(outputFile).use {
val outputStream = it
CipherOutputStream(outputStream, cipher).use {
inputStream.copyTo(it, 4096)
}
}
}
/* val cis = CipherOutputStream(outputStream, cipher)
var numberOfBytedRead: Int = 0
val buffer = ByteArray(4096)
while (numberOfBytedRead >= 0) {
cis.write(buffer, 0, numberOfBytedRead);
numberOfBytedRead = inputStream.read(buffer)
}
cis.flush()
cis.close()
inputStream.close()
outputStream.close()*/
} catch (ex: NoSuchPaddingException) {
// Log.d("TestingEnc","NoSuchPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: NoSuchAlgorithmException) {
// Log.d("TestingEnc","NoSuchAlgorithmException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: InvalidKeyException) {
//Log.d("TestingEnc","InvalidKeyException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: BadPaddingException) {
// Log.d("TestingEnc","BadPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IllegalBlockSizeException) {
// Log.d("TestingEnc","IllegalBlockSizeException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IOException) {
// Log.d("TestingEnc","IOException")
throw CryptoException("Error encrypting/decrypting file", ex)
}
}
@Throws(CryptoException::class)
private fun doCryptoDecrypt(
cipherMode: Int, key: String, inputFile: File,
outputFile: File
) {
try {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(cipherMode, secretKey)
FileInputStream(inputFile).use {
val inputStream = it
FileOutputStream(outputFile).use {
val outputStream = it
CipherInputStream(inputStream, cipher).use {
it.copyTo(outputStream, 4096)
}
}
}
/*var numberOfBytedRead: Int = 0
val buffer = ByteArray(4096)*/
/*while (numberOfBytedRead >= 0) {
outputStream.write(buffer, 0, numberOfBytedRead);
numberOfBytedRead = cin.read(buffer)
}*/
} catch (ex: NoSuchPaddingException) {
//Log.d("TestingEnc","NoSuchPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: NoSuchAlgorithmException) {
//Log.d("TestingEnc","NoSuchAlgorithmException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: InvalidKeyException) {
//Log.d("TestingEnc","InvalidKeyException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: BadPaddingException) {
//Log.d("TestingEnc","BadPaddingException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IllegalBlockSizeException) {
//Log.d("TestingEnc","IllegalBlockSizeException")
throw CryptoException("Error encrypting/decrypting file", ex)
} catch (ex: IOException) {
// Log.d("TestingEnc","IOException")
throw CryptoException("Error encrypting/decrypting file", ex)
}
}
}

View File

@@ -0,0 +1,123 @@
package com.yogeshpaliyal.keypass.ui.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.yogeshpaliyal.keypass.AppDatabase
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.databinding.FragmentDetailBinding
import com.yogeshpaliyal.keypass.utils.initViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 31-01-2021 10:38
*/
class DetailActivity : AppCompatActivity() {
lateinit var binding : FragmentDetailBinding
companion object{
private const val ARG_ACCOUNT_ID = "ARG_ACCOUNT_ID"
@JvmStatic
fun start(context: Context?, accountId: Long? = null) {
val starter = Intent(context, DetailActivity::class.java)
.putExtra(ARG_ACCOUNT_ID,accountId)
context?.startActivity(starter)
}
}
private val mViewModel by lazy {
initViewModel(DetailViewModel::class.java)
}
private val accountId by lazy {
intent?.extras?.getLong(ARG_ACCOUNT_ID) ?: -1
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = FragmentDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.lifecycleOwner = this
mViewModel.loadAccount(accountId)
mViewModel.accountModel.observe(this, Observer {
binding.accountData = it
})
if (accountId > 0) {
binding.bottomAppBar.replaceMenu(R.menu.bottom_app_bar_detail)
}
binding.bottomAppBar.setNavigationOnClickListener {
onBackPressed()
}
binding.bottomAppBar.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_delete){
deleteAccount()
return@setOnMenuItemClickListener true
}
return@setOnMenuItemClickListener false
}
binding.btnSave.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val model = mViewModel.accountModel.value
if (model != null) {
AppDatabase.getInstance().getDao().insertOrUpdateAccount(model)
}
withContext(Dispatchers.Main) {
onBackPressed()
}
}
}
}
private fun deleteAccount() {
MaterialAlertDialogBuilder(this)
.setTitle("Are you sure?")
.setMessage("Do you really want to delete this entry, it can't be restored")
.setPositiveButton("Delete"
) { dialog, which ->
dialog?.dismiss()
lifecycleScope.launch(Dispatchers.IO) {
if (accountId > 0L) {
AppDatabase.getInstance().getDao().deleteAccount(accountId)
}
withContext(Dispatchers.Main) {
onBackPressed()
}
}
}
.setNegativeButton("Cancel"){dialog, which ->
dialog.dismiss()
}.show()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.bottom_app_bar_detail, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return super.onOptionsItemSelected(item)
}
}

View File

@@ -1,68 +0,0 @@
package com.yogeshpaliyal.keypass.ui.detail
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.yogeshpaliyal.keypass.AppDatabase
import com.yogeshpaliyal.keypass.data.AccountModel
import com.yogeshpaliyal.keypass.databinding.FragmentDetailBinding
import com.yogeshpaliyal.keypass.utils.initViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 31-01-2021 10:38
*/
class DetailFragment : Fragment() {
lateinit var binding : FragmentDetailBinding
private val args: DetailFragmentArgs by navArgs()
private val mViewModel by lazy {
initViewModel(DetailViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDetailBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel.loadAccount(args.accountId)
mViewModel.accountModel.observe(viewLifecycleOwner, Observer {
binding.accountData = it
})
}
fun fabClicked(){
lifecycleScope.launch(Dispatchers.IO) {
val model = mViewModel.accountModel.value
if (model != null) {
AppDatabase.getInstance().getDao().insertOrUpdateAccount(model)
}
withContext(Dispatchers.Main) {
findNavController().popBackStack()
}
}
}
}

View File

@@ -20,11 +20,13 @@ class DetailViewModel(application: Application) : AndroidViewModel(application)
val accountModel by lazy { MutableLiveData<AccountModel>() }
fun loadAccount(accountId: Long?){
fun loadAccount(accountId: Long?) {
viewModelScope.launch(Dispatchers.IO) {
accountModel.postValue(AppDatabase.getInstance().getDao().getAccount(accountId) ?: AccountModel())
}
viewModelScope.launch(Dispatchers.IO) {
accountModel.postValue(
AppDatabase.getInstance().getDao().getAccount(accountId) ?: AccountModel()
)
}
}

View File

@@ -12,8 +12,7 @@ import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.data.AccountModel
import com.yogeshpaliyal.keypass.databinding.FragmentHomeBinding
import com.yogeshpaliyal.keypass.listener.UniversalClickListener
import com.yogeshpaliyal.keypass.ui.detail.DetailFragmentArgs
import com.yogeshpaliyal.keypass.ui.detail.DetailFragmentDirections
import com.yogeshpaliyal.keypass.ui.detail.DetailActivity
import com.yogeshpaliyal.keypass.utils.initViewModel
import com.yogeshpaliyal.universal_adapter.adapter.UniversalAdapterViewType
import com.yogeshpaliyal.universal_adapter.adapter.UniversalRecyclerAdapter
@@ -51,8 +50,7 @@ class HomeFragment : Fragment() {
val mListener = object : UniversalClickListener<AccountModel>{
override fun onItemClick(view: View, model: AccountModel) {
val destination = DetailFragmentDirections.actionGlobalCreateFragment(model.id ?: -1)
findNavController().navigate(destination)
DetailActivity.start(context, model.id)
}
}

View File

@@ -20,9 +20,7 @@ import com.google.android.material.transition.MaterialElevationScale
import com.yogeshpaliyal.keypass.AppDatabase
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.databinding.ActivityDashboardBinding
import com.yogeshpaliyal.keypass.ui.detail.DetailFragment
import com.yogeshpaliyal.keypass.ui.detail.DetailFragmentArgs
import com.yogeshpaliyal.keypass.ui.detail.DetailFragmentDirections
import com.yogeshpaliyal.keypass.ui.detail.DetailActivity
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity
import com.yogeshpaliyal.keypass.ui.home.HomeFragmentDirections
import com.yogeshpaliyal.keypass.ui.settings.MySettingsFragmentDirections
@@ -41,8 +39,6 @@ class DashboardActivity : AppCompatActivity(),
supportFragmentManager.findFragmentById(R.id.bottom_nav_drawer) as BottomNavDrawerFragment
}
private var currentAccountId = -1L
val currentNavigationFragment: Fragment?
get() = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
?.childFragmentManager
@@ -78,11 +74,6 @@ class DashboardActivity : AppCompatActivity(),
binding.btnAdd.setOnClickListener {
if (it.isActivated && currentNavigationFragment is DetailFragment) {
(currentNavigationFragment as DetailFragment).fabClicked()
return@setOnClickListener
}
currentNavigationFragment?.apply {
exitTransition = MaterialElevationScale(false).apply {
duration = resources.getInteger(R.integer.keypass_motion_duration_large).toLong()
@@ -92,8 +83,7 @@ class DashboardActivity : AppCompatActivity(),
}
}
val directions = DetailFragmentDirections.actionGlobalCreateFragment()
findNavController(R.id.nav_host_fragment).navigate(directions)
DetailActivity.start(this)
}
@@ -130,7 +120,6 @@ class DashboardActivity : AppCompatActivity(),
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.action_delete -> deleteAccount()
R.id.action_settings -> {
val settingDestination = MySettingsFragmentDirections.actionGlobalSettings()
findNavController(R.id.nav_host_fragment).navigate(settingDestination)
@@ -140,46 +129,18 @@ class DashboardActivity : AppCompatActivity(),
return true
}
private fun deleteAccount() {
MaterialAlertDialogBuilder(this)
.setTitle("Are you sure?")
.setMessage("Do you really want to delete this entry, it can't be restored")
.setPositiveButton("Delete"
) { dialog, which ->
dialog?.dismiss()
lifecycleScope.launch(Dispatchers.IO) {
if (currentAccountId != -1L) {
AppDatabase.getInstance().getDao().deleteAccount(currentAccountId)
}
withContext(Dispatchers.Main) {
findNavController(R.id.nav_host_fragment).popBackStack()
}
}
}
.setNegativeButton("Cancel"){dialog, which ->
dialog.dismiss()
}.show()
}
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
currentAccountId = -1
when (destination.id) {
R.id.homeFragment -> {
binding.btnAdd.isActivated = false
setBottomAppBarForHome(getBottomAppBarMenuForDestination(destination))
}
R.id.detailFragment -> {
binding.btnAdd.isActivated = true
currentAccountId =
if (arguments == null) -1 else DetailFragmentArgs.fromBundle(arguments).accountId
setBottomAppBarForHome(getBottomAppBarMenuForDestination(destination))
}
}
}
@@ -241,7 +202,6 @@ class DashboardActivity : AppCompatActivity(),
val dest = destination ?: findNavController(R.id.nav_host_fragment).currentDestination
return when (dest?.id) {
R.id.homeFragment -> R.menu.bottom_app_bar_settings_menu
R.id.detailFragment -> if (currentAccountId > 0) R.menu.bottom_app_bar_detail else R.menu.bottom_app_bar_settings_menu
//R.id.emailFragment -> R.menu.bottom_app_bar_email_menu
else -> R.menu.bottom_app_bar_settings_menu
}

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnPrimary"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M11.67,3.87L9.9,2.1 0,12l9.9,9.9 1.77,-1.77L3.54,12z"/>
</vector>

View File

@@ -5,13 +5,20 @@
name="accountData"
type="com.yogeshpaliyal.keypass.data.AccountModel" />
</data>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingHorizontal="@dimen/grid_0_5"
android:paddingVertical="@dimen/grid_0_5"
android:id="@+id/nestedScrollView">
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:fillViewport="true"
android:id="@+id/nestedScrollView"
android:paddingHorizontal="@dimen/grid_0_5"
android:paddingVertical="@dimen/grid_0_5">
<com.yogeshpaliyal.keypass.custom_views.MaskedCardView
android:layout_width="match_parent"
@@ -174,8 +181,29 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</com.yogeshpaliyal.keypass.custom_views.MaskedCardView>
</androidx.core.widget.NestedScrollView>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:fabAlignmentMode="end"
app:hideOnScroll="true"
app:fabCradleRoundedCornerRadius="@dimen/bottom_app_bar_fab_cradle_corner_radius"
app:fabCradleMargin="@dimen/bottom_app_bar_fab_cradle_margin"
app:navigationIcon="@drawable/ic_baseline_arrow_back_ios_24"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_round_done_24"
android:layout_marginEnd="@dimen/grid_2"
app:layout_anchor="@id/bottomAppBar"
android:id="@+id/btnSave"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -17,7 +17,7 @@
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.yogeshpaliyal.keypass.ui.detail.DetailFragment"
android:name="com.yogeshpaliyal.keypass.ui.detail.DetailActivity"
android:label="DetailFragment"
tools:layout="@layout/fragment_detail">
<argument android:name="accountId"