diff options
Diffstat (limited to 'app/src/main/java')
6 files changed, 227 insertions, 188 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/OnboardingActivity.kt b/app/src/main/java/com/zeapo/pwdstore/OnboardingActivity.kt new file mode 100644 index 00000000..d2f19b7a --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/OnboardingActivity.kt @@ -0,0 +1,203 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore + +import android.Manifest +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import com.github.ajalt.timberkt.d +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.databinding.ActivityOnboardingBinding +import com.zeapo.pwdstore.git.BaseGitActivity +import com.zeapo.pwdstore.git.GitServerConfigActivity +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.checkRuntimePermission +import com.zeapo.pwdstore.utils.getString +import com.zeapo.pwdstore.utils.listFilesRecursively +import com.zeapo.pwdstore.utils.sharedPrefs +import com.zeapo.pwdstore.utils.viewBinding +import java.io.File + +class OnboardingActivity : AppCompatActivity() { + + private val binding by viewBinding(ActivityOnboardingBinding::inflate) + private val settings by lazy { applicationContext.sharedPrefs } + private val sortOrder: PasswordRepository.PasswordSortOrder + get() = PasswordRepository.PasswordSortOrder.getSortOrder(settings) + + private val cloneAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + finish() + } + } + + private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + initializeRepositoryInfo() + finish() + } + } + + private val externalDirectorySelectAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + if (checkExternalDirectory()) { + finish() + } else { + createRepository() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.hide() + setContentView(binding.root) + binding.settingsButton.setOnClickListener { + startActivity(Intent(this, UserPreference::class.java)) + } + binding.localDirectoryButton.setOnClickListener { + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.location_dialog_title)) + .setMessage(resources.getString(R.string.location_dialog_create_text)) + .setPositiveButton(resources.getString(R.string.location_hidden)) { _, _ -> + createRepoInHiddenDir() + } + .setNegativeButton(resources.getString(R.string.location_sdcard)) { _, _ -> + createRepoFromExternalDir() + } + .show() + } + binding.cloneFromServerButton.setOnClickListener { + cloneToHiddenDir() + } + } + + /** + * Clones a remote Git repository to the app's private directory + */ + private fun cloneToHiddenDir() { + settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) } + cloneAction.launch(Intent(this, GitServerConfigActivity::class.java).apply { + putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + }) + } + + /** + * Initializes an empty repository in the app's private directory + */ + private fun createRepoInHiddenDir() { + settings.edit { + putBoolean(PreferenceKeys.GIT_EXTERNAL, false) + remove(PreferenceKeys.GIT_EXTERNAL_REPO) + } + initializeRepositoryInfo() + finish() + } + + /** + * Initializes an empty repository in a selected directory if one does not already exist + */ + private fun createRepoFromExternalDir() { + settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) } + val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + if (externalRepo == null) { + if (!checkRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + registerForActivityResult(RequestPermission()) { granted -> + if (granted) { + externalDirectorySelectAction.launch(Intent(this, UserPreference::class.java).apply { + putExtra("operation", "git_external") + }) + } + }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } else { + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.directory_selected_title)) + .setMessage(resources.getString(R.string.directory_selected_message, externalRepo)) + .setPositiveButton(resources.getString(R.string.use)) { _, _ -> + if (!checkRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + registerForActivityResult(RequestPermission()) { granted -> + if (granted) { + initializeRepositoryInfo() + } + }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + initializeRepositoryInfo() + } + } + .setNegativeButton(resources.getString(R.string.change)) { _, _ -> + if (!checkRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + registerForActivityResult(RequestPermission()) { granted -> + if (granted) { + repositoryInitAction.launch(Intent(this, UserPreference::class.java).apply { + putExtra("operation", "git_external") + }) + } + }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + .show() + } + } + + private fun checkExternalDirectory(): Boolean { + if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) && + settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null) { + val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + val dir = externalRepoPath?.let { File(it) } + if (dir != null && // The directory could be opened + dir.exists() && // The directory exists + dir.isDirectory && // The directory, is really a directory + dir.listFilesRecursively().isNotEmpty() && // The directory contains files + // The directory contains a non-zero number of password files + PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(), sortOrder).isNotEmpty() + ) { + PasswordRepository.closeRepository() + return true + } + } + return false + } + + private fun createRepository() { + val localDir = PasswordRepository.getRepositoryDirectory() + try { + check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" } + PasswordRepository.createRepository(localDir) + if (!PasswordRepository.isInitialized) { + PasswordRepository.initialize() + } + if (File(localDir.absolutePath + "/.gpg-id").createNewFile()) { + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + } else { + throw IllegalStateException("Failed to initialize repository state.") + } + } catch (e: Exception) { + e.printStackTrace() + if (!localDir.delete()) { + d { "Failed to delete local repository: $localDir" } + } + } + } + + private fun initializeRepositoryInfo() { + val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) + val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + if (externalRepo && !checkRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + return + } + if (externalRepo && externalRepoPath != null) { + if (checkExternalDirectory()) return + } + createRepository() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index 7f8c4e55..56a6b949 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -8,7 +8,6 @@ import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.content.pm.ShortcutInfo.Builder import android.content.pm.ShortcutManager import android.graphics.drawable.Icon @@ -26,7 +25,6 @@ import androidx.activity.viewModels import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView.OnQueryTextListener -import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.content.getSystemService import androidx.fragment.app.FragmentManager @@ -54,16 +52,13 @@ import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment import com.zeapo.pwdstore.utils.PasswordItem import com.zeapo.pwdstore.utils.PasswordRepository -import com.zeapo.pwdstore.utils.PasswordRepository.Companion.closeRepository -import com.zeapo.pwdstore.utils.PasswordRepository.Companion.createRepository -import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getPasswords import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepository import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory import com.zeapo.pwdstore.utils.PasswordRepository.Companion.initialize import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized -import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.base64 +import com.zeapo.pwdstore.utils.checkRuntimePermission import com.zeapo.pwdstore.utils.commitChange import com.zeapo.pwdstore.utils.contains import com.zeapo.pwdstore.utils.getString @@ -94,43 +89,9 @@ class PasswordStore : BaseGitActivity() { ViewModelProvider.AndroidViewModelFactory(application) } - private val cloneAction = registerForActivityResult(StartActivityForResult()) { result -> + private val listRefreshAction = registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - } - } - - private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - initializeRepositoryInfo() - } - } - - private val directoryChangeAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) && - settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null) { - val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - val dir = externalRepoPath?.let { File(it) } - if (dir != null && - dir.exists() && - dir.isDirectory && - dir.listFilesRecursively().isNotEmpty() && - getPasswords(dir, getRepositoryDirectory(), sortOrder).isNotEmpty()) { - closeRepository() - checkLocalRepository() - return@registerForActivityResult - } - } - checkPermissionsAndCloneAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - } - - private val checkPermissionsAndCloneAction = registerForActivityResult(RequestPermission()) { granted -> - if (granted) { - val intent = Intent(activity, GitServerConfigActivity::class.java) - intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE) - cloneAction.launch(intent) + refreshPasswordList() } } @@ -160,9 +121,7 @@ class PasswordStore : BaseGitActivity() { // prevent attempt to create password list fragment var savedInstance = savedInstanceState if (savedInstanceState != null && (!settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) || - ContextCompat.checkSelfPermission( - activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED)) { + !checkRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))) { savedInstance = null } super.onCreate(savedInstance) @@ -229,7 +188,7 @@ class PasswordStore : BaseGitActivity() { override fun onResume() { super.onResume() if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) { - hasRequiredStoragePermissions(true) + hasRequiredStoragePermissions() } else { checkLocalRepository() } @@ -361,47 +320,6 @@ class PasswordStore : BaseGitActivity() { searchItem.collapseActionView() } - private fun createRepository() { - if (!isInitialized) { - initialize() - } - val localDir = getRepositoryDirectory() - try { - check(localDir.mkdir()) { "Failed to create directory!" } - createRepository(localDir) - if (File(localDir.absolutePath + "/.gpg-id").createNewFile()) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - } else { - throw IllegalStateException("Failed to initialize repository state.") - } - } catch (e: Exception) { - e.printStackTrace() - if (!localDir.delete()) { - d { "Failed to delete local repository" } - } - return - } - checkLocalRepository() - } - - private fun initializeRepositoryInfo() { - val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) - val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - if (externalRepo && !hasRequiredStoragePermissions()) { - return - } - if (externalRepo && externalRepoPath != null) { - val dir = File(externalRepoPath) - if (dir.exists() && dir.isDirectory && - getPasswords(dir, getRepositoryDirectory(), sortOrder).isNotEmpty()) { - closeRepository() - checkLocalRepository() - return // if not empty, just show me the passwords! - } - } - createRepository() - } - private fun runGitOperation(operation: Int) = lifecycleScope.launch { launchGitOperation(operation).fold( success = { refreshPasswordList() }, @@ -413,10 +331,8 @@ class PasswordStore : BaseGitActivity() { * Validates if storage permission is granted, and requests for it if not. The return value * is true if the permission has been granted. */ - private fun hasRequiredStoragePermissions(checkLocalRepo: Boolean = false): Boolean { - val cloning = supportFragmentManager.findFragmentByTag("ToCloneOrNot") != null - return if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED && !cloning) { + private fun hasRequiredStoragePermissions(): Boolean { + return if (!checkRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { Snackbar.make( findViewById(R.id.main_layout), getString(R.string.access_sdcard_text), @@ -432,8 +348,7 @@ class PasswordStore : BaseGitActivity() { } false } else { - if (checkLocalRepo) - checkLocalRepository() + checkLocalRepository() true } } @@ -478,11 +393,7 @@ class PasswordStore : BaseGitActivity() { } } } else { - supportActionBar!!.hide() - supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - supportFragmentManager.commit { - replace(R.id.main_layout, ToCloneOrNot(), "ToCloneOrNot") - } + startActivity(Intent(this, OnboardingActivity::class.java)) } } @@ -821,60 +732,6 @@ class PasswordStore : BaseGitActivity() { } } - fun initRepository(operation: Int) { - closeRepository() - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.location_dialog_title)) - .setMessage(resources.getString(R.string.location_dialog_text)) - .setPositiveButton(resources.getString(R.string.location_hidden)) { _, _ -> - settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) } - when (operation) { - NEW_REPO_BUTTON -> initializeRepositoryInfo() - CLONE_REPO_BUTTON -> { - val intent = Intent(activity, GitServerConfigActivity::class.java) - intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE) - cloneAction.launch(intent) - } - } - } - .setNegativeButton(resources.getString(R.string.location_sdcard)) { _, _ -> - settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) } - val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - if (externalRepo == null) { - val intent = Intent(activity, UserPreference::class.java) - intent.putExtra("operation", "git_external") - when (operation) { - NEW_REPO_BUTTON -> repositoryInitAction.launch(intent) - CLONE_REPO_BUTTON -> directoryChangeAction.launch(intent) - } - } else { - MaterialAlertDialogBuilder(activity) - .setTitle(resources.getString(R.string.directory_selected_title)) - .setMessage(resources.getString(R.string.directory_selected_message, externalRepo)) - .setPositiveButton(resources.getString(R.string.use)) { _, _ -> - when (operation) { - NEW_REPO_BUTTON -> initializeRepositoryInfo() - CLONE_REPO_BUTTON -> { - val intent = Intent(activity, GitServerConfigActivity::class.java) - intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE) - cloneAction.launch(intent) - } - } - } - .setNegativeButton(resources.getString(R.string.change)) { _, _ -> - val intent = Intent(activity, UserPreference::class.java) - intent.putExtra("operation", "git_external") - when (operation) { - NEW_REPO_BUTTON -> repositoryInitAction.launch(intent) - CLONE_REPO_BUTTON -> directoryChangeAction.launch(intent) - } - } - .show() - } - } - .show() - } - fun matchPasswordWithApp(item: PasswordItem) { val path = item.file .absolutePath @@ -886,14 +743,9 @@ class PasswordStore : BaseGitActivity() { finish() } - private val sortOrder: PasswordRepository.PasswordSortOrder - get() = getSortOrder(settings) - companion object { const val REQUEST_ARG_PATH = "PATH" - const val CLONE_REPO_BUTTON = 401 - const val NEW_REPO_BUTTON = 402 private fun isPrintable(c: Char): Boolean { val block = UnicodeBlock.of(c) return (!Character.isISOControl(c) && diff --git a/app/src/main/java/com/zeapo/pwdstore/ToCloneOrNot.kt b/app/src/main/java/com/zeapo/pwdstore/ToCloneOrNot.kt deleted file mode 100644 index 66a450f0..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/ToCloneOrNot.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore - -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import com.zeapo.pwdstore.databinding.FragmentToCloneOrNotBinding -import com.zeapo.pwdstore.utils.viewBinding - -class ToCloneOrNot : Fragment(R.layout.fragment_to_clone_or_not) { - - private val binding by viewBinding(FragmentToCloneOrNotBinding::bind) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), UserPreference::class.java)) } - binding.localDirectoryButton.setOnClickListener { (requireActivity() as PasswordStore).initRepository(PasswordStore.NEW_REPO_BUTTON) } - binding.cloneFromServerButton.setOnClickListener { (requireActivity() as PasswordStore).initRepository(PasswordStore.CLONE_REPO_BUTTON) } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt index 27cd6379..d867b4db 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt @@ -124,7 +124,10 @@ class GitServerConfigActivity : BaseGitActivity() { } snackbar.dismiss() launchGitOperation(REQUEST_CLONE).fold( - success = ::finishOnSuccessHandler, + success = { + setResult(RESULT_OK) + finish() + }, failure = ::finishAfterPromptOnErrorHandler, ) } @@ -159,7 +162,10 @@ class GitServerConfigActivity : BaseGitActivity() { } lifecycleScope.launch { launchGitOperation(REQUEST_CLONE).fold( - success = ::finishOnSuccessHandler, + success = { + setResult(RESULT_OK) + finish() + }, failure = ::finishAfterPromptOnErrorHandler, ) } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index eea69dc4..ab78e98f 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -8,6 +8,7 @@ import android.app.KeyguardManager import android.content.ClipboardManager import android.content.Context import android.content.SharedPreferences +import android.content.pm.PackageManager import android.os.Build import android.util.Base64 import android.util.TypedValue @@ -17,6 +18,7 @@ import android.view.inputmethod.InputMethodManager import androidx.annotation.IdRes import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager @@ -131,6 +133,10 @@ suspend fun FragmentActivity.commitChange( }.execute() } +fun FragmentActivity.checkRuntimePermission(permission: String): Boolean { + return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED +} + /** * Extension function for [AlertDialog] that requests focus for the * view whose id is [id]. Solution based on a StackOverflow diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt index 73660fca..3d0d4606 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt @@ -20,7 +20,6 @@ import org.eclipse.jgit.transport.URIish open class PasswordRepository protected constructor() { - @Suppress("Unused") enum class PasswordSortOrder(val comparator: Comparator<PasswordItem>) { FOLDER_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem -> @@ -93,10 +92,7 @@ open class PasswordRepository protected constructor() { @JvmStatic fun isGitRepo(): Boolean { if (repository != null) { - // Check if remote exists - return repository!!.config.getSubsections("remote").isNotEmpty() && - repository!!.objectDatabase.exists() && - repository!!.allRefs.isNotEmpty() + return repository!!.objectDatabase.exists() } return false } |