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 - 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

View file

@ -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) &&

View file

@ -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)
} }

View file

@ -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()
} }
} }
} }

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.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 {
lifecycleScope.launch {
val snackbar = snackbar(message = getString(R.string.delete_directory_progress_text), length = Snackbar.LENGTH_INDEFINITE)
withContext(Dispatchers.IO) {
localDir.deleteRecursively() localDir.deleteRecursively()
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) } }
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()

View file

@ -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)

View file

@ -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>