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:
Harsh Shandilya 2020-08-16 02:16:02 +05:30 committed by GitHub
parent 7bb40d109a
commit 2ffd1abb27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 39 additions and 31 deletions

View file

@ -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
- Fix crash when OpenKeychain is not installed
- 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

View file

@ -19,13 +19,13 @@ import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MenuItem.OnActionExpandListener
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.content.getSystemService
@ -127,7 +127,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
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)
cloneAction.launch(intent)
}
@ -220,14 +226,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
}
}
public override fun onStart() {
override fun onStart() {
super.onStart()
refreshPasswordList()
}
public override fun onResume() {
override fun onResume() {
super.onResume()
// do not attempt to checkLocalRepository() if no storage permission: immediate crash
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
hasRequiredStoragePermissions(true)
} 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 {
val menuRes = when {
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.
*/
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) {
!= PackageManager.PERMISSION_GRANTED && !cloning) {
Snackbar.make(
findViewById(R.id.main_layout),
getString(R.string.access_sdcard_text),
Snackbar.LENGTH_INDEFINITE
).run {
setAction(getString(R.string.snackbar_action_grant)) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_EXTERNAL_STORAGE
)
registerForActivityResult(RequestPermission()) { granted ->
if (granted) checkLocalRepository()
}.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
dismiss()
}
show()
@ -491,7 +485,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
supportActionBar!!.hide()
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
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 CLONE_REPO_BUTTON = 401
const val NEW_REPO_BUTTON = 402
private const val REQUEST_EXTERNAL_STORAGE = 50
private fun isPrintable(c: Char): Boolean {
val block = UnicodeBlock.of(c)
return (!Character.isISOControl(c) &&

View file

@ -464,7 +464,7 @@ class UserPreference : AppCompatActivity() {
when (intent?.getStringExtra("operation")) {
"get_ssh_key" -> getSshKey()
"make_ssh_key" -> makeSshKey(false)
"git_external" -> selectExternalGitRepository()
"git_external" -> selectExternalGitRepository(fromIntent = true)
}
prefsFragment = PrefsFragment()
@ -477,7 +477,7 @@ class UserPreference : AppCompatActivity() {
}
@Suppress("Deprecation") // for Environment.getExternalStorageDirectory()
fun selectExternalGitRepository() {
fun selectExternalGitRepository(fromIntent: Boolean = false) {
MaterialAlertDialogBuilder(this)
.setTitle(this.resources.getString(R.string.external_repository_dialog_title))
.setMessage(this.resources.getString(R.string.external_repository_dialog_text))
@ -506,6 +506,10 @@ class UserPreference : AppCompatActivity() {
.show()
}
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) }
if (fromIntent) {
setResult(RESULT_OK)
finish()
}
}.launch(null)
}

View file

@ -20,13 +20,12 @@ open class GitOperationActivity : BaseGitActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
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_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
else -> {
setResult(RESULT_CANCELED)
finish()
throw IllegalArgumentException("Invalid request code: $reqCode")
}
}
}

View file

@ -17,9 +17,12 @@ import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.snackbar
import com.zeapo.pwdstore.utils.viewBinding
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* 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)
.setPositiveButton(R.string.dialog_delete) { dialog, _ ->
try {
localDir.deleteRecursively()
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
lifecycleScope.launch {
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) {
// TODO Handle the exception correctly if we are unable to delete the directory...
e.printStackTrace()

View file

@ -169,7 +169,7 @@ open class PasswordRepository protected constructor() {
val dir = getRepositoryDirectory()
// uninitialize the repo if the dir does not exist or is absolutely empty
settings.edit {
if (!dir.exists() || !dir.isDirectory || dir.listFiles()!!.isEmpty()) {
if (!dir.exists() || !dir.isDirectory || dir.listFiles()?.isEmpty() == true) {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
} else {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true)

View file

@ -27,6 +27,7 @@
<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>
</plurals>
<string name="delete_directory_progress_text">Deleting…</string>
<string name="move">Move</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>