diff --git a/app/build.gradle b/app/build.gradle index 3fff1a76..2b0a2cc3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6c8a3da..619d086f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,8 @@ + ${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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt new file mode 100644 index 00000000..545d5955 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailFragment.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailFragment.kt deleted file mode 100644 index 9ee95551..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailFragment.kt +++ /dev/null @@ -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() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt index e5f569d4..9121eabe 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt @@ -20,11 +20,13 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) val accountModel by lazy { MutableLiveData() } - 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() + ) + } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/HomeFragment.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/HomeFragment.kt index 8986d48b..155ad9d7 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/HomeFragment.kt @@ -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{ override fun onItemClick(view: View, model: AccountModel) { - val destination = DetailFragmentDirections.actionGlobalCreateFragment(model.id ?: -1) - findNavController().navigate(destination) + DetailActivity.start(context, model.id) } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivity.kt index 29130122..c2900a20 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivity.kt @@ -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 } diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml new file mode 100644 index 00000000..687e5c9b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index e01b295a..9b8b5a58 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -5,13 +5,20 @@ name="accountData" type="com.yogeshpaliyal.keypass.data.AccountModel" /> - + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/navigation_graph.xml b/app/src/main/res/navigation/navigation_graph.xml index 19d787d2..b44a074e 100644 --- a/app/src/main/res/navigation/navigation_graph.xml +++ b/app/src/main/res/navigation/navigation_graph.xml @@ -17,7 +17,7 @@