Fix external storage UX (#1022)
* build: update to Kotlin 1.4
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* UserPreference: finish if directory selection was triggered from an intent
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* PasswordStore: switch permission request to ActivityResultContracts
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* PasswordStore: fix activity reference
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* GitOperationActivity: make invalid values more obvious
Would have caught this issue much sooner if I had just done this
Fixes: 3d8cea5966
("Improve permission handling logic (#732)")
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* Assorted collection of hackery to make external storage use palatable
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* Update changelog
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
7bb40d109a
commit
2ffd1abb27
7 changed files with 39 additions and 31 deletions
|
@ -22,6 +22,8 @@ All notable changes to this project will be documented in this file.
|
||||||
- I keep saying this but for real: error message for wrong SSH/HTTPS password is properly fixed now
|
- I keep saying this but for real: error message for wrong SSH/HTTPS password is properly fixed now
|
||||||
- Fix crash when OpenKeychain is not installed
|
- Fix crash when OpenKeychain is not installed
|
||||||
- Clone operation won't leave user on an empty password list upon failure
|
- Clone operation won't leave user on an empty password list upon failure
|
||||||
|
- Cloning a new repository to external storage wouldn't work
|
||||||
|
- UI froze for some people when deleting existing files from the external directory
|
||||||
|
|
||||||
## [1.10.3] - 2020-07-30
|
## [1.10.3] - 2020-07-30
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,13 @@ import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MenuItem.OnActionExpandListener
|
import android.view.MenuItem.OnActionExpandListener
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
@ -127,7 +127,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val intent = Intent(activity, GitOperationActivity::class.java)
|
checkPermissionsAndCloneAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val checkPermissionsAndCloneAction = registerForActivityResult(RequestPermission()) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
val intent = Intent(activity, GitServerConfigActivity::class.java)
|
||||||
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
|
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
|
||||||
cloneAction.launch(intent)
|
cloneAction.launch(intent)
|
||||||
}
|
}
|
||||||
|
@ -220,14 +226,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
refreshPasswordList()
|
refreshPasswordList()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
// do not attempt to checkLocalRepository() if no storage permission: immediate crash
|
|
||||||
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
|
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
|
||||||
hasRequiredStoragePermissions(true)
|
hasRequiredStoragePermissions(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,16 +245,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
// If request is cancelled, the result arrays are empty.
|
|
||||||
if (requestCode == REQUEST_EXTERNAL_STORAGE) {
|
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
checkLocalRepository()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
val menuRes = when {
|
val menuRes = when {
|
||||||
GitSettings.connectionMode == ConnectionMode.None -> R.menu.main_menu_no_auth
|
GitSettings.connectionMode == ConnectionMode.None -> R.menu.main_menu_no_auth
|
||||||
|
@ -423,19 +418,18 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
* is true if the permission has been granted.
|
* is true if the permission has been granted.
|
||||||
*/
|
*/
|
||||||
private fun hasRequiredStoragePermissions(checkLocalRepo: Boolean = false): Boolean {
|
private fun hasRequiredStoragePermissions(checkLocalRepo: Boolean = false): Boolean {
|
||||||
|
val cloning = supportFragmentManager.findFragmentByTag("ToCloneOrNot") != null
|
||||||
return if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
return if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
!= PackageManager.PERMISSION_GRANTED && !cloning) {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
findViewById(R.id.main_layout),
|
findViewById(R.id.main_layout),
|
||||||
getString(R.string.access_sdcard_text),
|
getString(R.string.access_sdcard_text),
|
||||||
Snackbar.LENGTH_INDEFINITE
|
Snackbar.LENGTH_INDEFINITE
|
||||||
).run {
|
).run {
|
||||||
setAction(getString(R.string.snackbar_action_grant)) {
|
setAction(getString(R.string.snackbar_action_grant)) {
|
||||||
ActivityCompat.requestPermissions(
|
registerForActivityResult(RequestPermission()) { granted ->
|
||||||
activity,
|
if (granted) checkLocalRepository()
|
||||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
}.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
REQUEST_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
show()
|
show()
|
||||||
|
@ -491,7 +485,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
supportActionBar!!.hide()
|
supportActionBar!!.hide()
|
||||||
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
replace(R.id.main_layout, ToCloneOrNot())
|
replace(R.id.main_layout, ToCloneOrNot(), "ToCloneOrNot")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -903,7 +897,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
const val REQUEST_ARG_PATH = "PATH"
|
const val REQUEST_ARG_PATH = "PATH"
|
||||||
const val CLONE_REPO_BUTTON = 401
|
const val CLONE_REPO_BUTTON = 401
|
||||||
const val NEW_REPO_BUTTON = 402
|
const val NEW_REPO_BUTTON = 402
|
||||||
private const val REQUEST_EXTERNAL_STORAGE = 50
|
|
||||||
private fun isPrintable(c: Char): Boolean {
|
private fun isPrintable(c: Char): Boolean {
|
||||||
val block = UnicodeBlock.of(c)
|
val block = UnicodeBlock.of(c)
|
||||||
return (!Character.isISOControl(c) &&
|
return (!Character.isISOControl(c) &&
|
||||||
|
|
|
@ -464,7 +464,7 @@ class UserPreference : AppCompatActivity() {
|
||||||
when (intent?.getStringExtra("operation")) {
|
when (intent?.getStringExtra("operation")) {
|
||||||
"get_ssh_key" -> getSshKey()
|
"get_ssh_key" -> getSshKey()
|
||||||
"make_ssh_key" -> makeSshKey(false)
|
"make_ssh_key" -> makeSshKey(false)
|
||||||
"git_external" -> selectExternalGitRepository()
|
"git_external" -> selectExternalGitRepository(fromIntent = true)
|
||||||
}
|
}
|
||||||
prefsFragment = PrefsFragment()
|
prefsFragment = PrefsFragment()
|
||||||
|
|
||||||
|
@ -477,7 +477,7 @@ class UserPreference : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("Deprecation") // for Environment.getExternalStorageDirectory()
|
@Suppress("Deprecation") // for Environment.getExternalStorageDirectory()
|
||||||
fun selectExternalGitRepository() {
|
fun selectExternalGitRepository(fromIntent: Boolean = false) {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(this.resources.getString(R.string.external_repository_dialog_title))
|
.setTitle(this.resources.getString(R.string.external_repository_dialog_title))
|
||||||
.setMessage(this.resources.getString(R.string.external_repository_dialog_text))
|
.setMessage(this.resources.getString(R.string.external_repository_dialog_text))
|
||||||
|
@ -506,6 +506,10 @@ class UserPreference : AppCompatActivity() {
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) }
|
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) }
|
||||||
|
if (fromIntent) {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
}.launch(null)
|
}.launch(null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,12 @@ open class GitOperationActivity : BaseGitActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
when (intent.extras?.getInt(REQUEST_ARG_OP)) {
|
when (val reqCode = intent.extras?.getInt(REQUEST_ARG_OP)) {
|
||||||
REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
|
REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
|
||||||
REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
|
REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
|
||||||
REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
|
REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
|
||||||
else -> {
|
else -> {
|
||||||
setResult(RESULT_CANCELED)
|
throw IllegalArgumentException("Invalid request code: $reqCode")
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,12 @@ import com.zeapo.pwdstore.git.config.ConnectionMode
|
||||||
import com.zeapo.pwdstore.git.config.GitSettings
|
import com.zeapo.pwdstore.git.config.GitSettings
|
||||||
import com.zeapo.pwdstore.git.config.Protocol
|
import com.zeapo.pwdstore.git.config.Protocol
|
||||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||||
|
import com.zeapo.pwdstore.utils.snackbar
|
||||||
import com.zeapo.pwdstore.utils.viewBinding
|
import com.zeapo.pwdstore.utils.viewBinding
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity that encompasses both the initial clone as well as editing the server config for future
|
* Activity that encompasses both the initial clone as well as editing the server config for future
|
||||||
|
@ -125,8 +128,14 @@ class GitServerConfigActivity : BaseGitActivity() {
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.dialog_delete) { dialog, _ ->
|
.setPositiveButton(R.string.dialog_delete) { dialog, _ ->
|
||||||
try {
|
try {
|
||||||
localDir.deleteRecursively()
|
lifecycleScope.launch {
|
||||||
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
|
val snackbar = snackbar(message = getString(R.string.delete_directory_progress_text), length = Snackbar.LENGTH_INDEFINITE)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
localDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
snackbar.dismiss()
|
||||||
|
launchGitOperation(REQUEST_CLONE)
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// TODO Handle the exception correctly if we are unable to delete the directory...
|
// TODO Handle the exception correctly if we are unable to delete the directory...
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
|
@ -169,7 +169,7 @@ open class PasswordRepository protected constructor() {
|
||||||
val dir = getRepositoryDirectory()
|
val dir = getRepositoryDirectory()
|
||||||
// uninitialize the repo if the dir does not exist or is absolutely empty
|
// uninitialize the repo if the dir does not exist or is absolutely empty
|
||||||
settings.edit {
|
settings.edit {
|
||||||
if (!dir.exists() || !dir.isDirectory || dir.listFiles()!!.isEmpty()) {
|
if (!dir.exists() || !dir.isDirectory || dir.listFiles()?.isEmpty() == true) {
|
||||||
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
|
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
|
||||||
} else {
|
} else {
|
||||||
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true)
|
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true)
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
<item quantity="one">Are you sure you want to delete the password?</item>
|
<item quantity="one">Are you sure you want to delete the password?</item>
|
||||||
<item quantity="other">Are you sure you want to delete %d passwords?</item>
|
<item quantity="other">Are you sure you want to delete %d passwords?</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="delete_directory_progress_text">Deleting…</string>
|
||||||
<string name="move">Move</string>
|
<string name="move">Move</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
|
Loading…
Reference in a new issue