Use storage access framework (#469)
* use storage access framework * UserPreference: Add back warning about using SDCard root directory * UserPreference: Fix IDE warnings Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
This commit is contained in:
parent
f272e4dde2
commit
e54010906f
6 changed files with 204 additions and 245 deletions
|
@ -74,7 +74,6 @@ dependencies {
|
||||||
implementation("com.google.android.material:material:1.0.0")
|
implementation("com.google.android.material:material:1.0.0")
|
||||||
implementation("androidx.annotation:annotation:1.0.2")
|
implementation("androidx.annotation:annotation:1.0.2")
|
||||||
implementation("org.sufficientlysecure:openpgp-api:12.0")
|
implementation("org.sufficientlysecure:openpgp-api:12.0")
|
||||||
implementation("com.nononsenseapps:filepicker:2.4.2")
|
|
||||||
implementation("org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r") {
|
implementation("org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r") {
|
||||||
exclude(group = "org.apache.httpcomponents", module = "httpclient")
|
exclude(group = "org.apache.httpcomponents", module = "httpclient")
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,15 +68,6 @@
|
||||||
<meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.zeapo.pwdstore.PasswordStore" />
|
<meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.zeapo.pwdstore.PasswordStore" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="com.nononsenseapps.filepicker.FilePickerActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/FilePickerTheme">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.GET_CONTENT" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".crypto.PgpActivity"
|
<activity android:name=".crypto.PgpActivity"
|
||||||
android:parentActivityName=".PasswordStore"/>
|
android:parentActivityName=".PasswordStore"/>
|
||||||
<activity android:name=".SelectFolderActivity" />
|
<activity android:name=".SelectFolderActivity" />
|
||||||
|
|
|
@ -1,46 +1,40 @@
|
||||||
package com.zeapo.pwdstore
|
package com.zeapo.pwdstore
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.preference.CheckBoxPreference
|
import android.preference.CheckBoxPreference
|
||||||
import android.preference.Preference
|
import android.preference.Preference
|
||||||
import android.preference.PreferenceFragment
|
import android.preference.PreferenceFragment
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
|
||||||
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
|
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
|
||||||
import com.zeapo.pwdstore.crypto.PgpActivity
|
import com.zeapo.pwdstore.crypto.PgpActivity
|
||||||
import com.zeapo.pwdstore.git.GitActivity
|
import com.zeapo.pwdstore.git.GitActivity
|
||||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import org.openintents.openpgp.util.OpenPgpUtils
|
import org.openintents.openpgp.util.OpenPgpUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.time.LocalDateTime
|
||||||
import java.util.ArrayList
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Date
|
import java.util.*
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class UserPreference : AppCompatActivity() {
|
class UserPreference : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var prefsFragment: PrefsFragment
|
private lateinit var prefsFragment: PrefsFragment
|
||||||
|
|
||||||
class PrefsFragment : PreferenceFragment() {
|
class PrefsFragment : PreferenceFragment() {
|
||||||
|
@ -61,7 +55,7 @@ class UserPreference : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference("ssh_key").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
findPreference("ssh_key").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
callingActivity.getSshKeyWithPermissions(sharedPreferences.getBoolean("use_android_file_picker", false))
|
callingActivity.getSshKey()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,18 +71,18 @@ class UserPreference : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference("ssh_key_clear_passphrase").onPreferenceClickListener =
|
findPreference("ssh_key_clear_passphrase").onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
sharedPreferences.edit().putString("ssh_key_passphrase", null).apply()
|
sharedPreferences.edit().putString("ssh_key_passphrase", null).apply()
|
||||||
it.isEnabled = false
|
it.isEnabled = false
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference("hotp_remember_clear_choice").onPreferenceClickListener =
|
findPreference("hotp_remember_clear_choice").onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply()
|
sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply()
|
||||||
it.isEnabled = false
|
it.isEnabled = false
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
val intent = Intent(callingActivity, GitActivity::class.java)
|
val intent = Intent(callingActivity, GitActivity::class.java)
|
||||||
|
@ -107,30 +101,30 @@ class UserPreference : AppCompatActivity() {
|
||||||
findPreference("git_delete_repo").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
findPreference("git_delete_repo").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
val repoDir = PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)
|
val repoDir = PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)
|
||||||
AlertDialog.Builder(callingActivity)
|
AlertDialog.Builder(callingActivity)
|
||||||
.setTitle(R.string.pref_dialog_delete_title)
|
.setTitle(R.string.pref_dialog_delete_title)
|
||||||
.setMessage(resources.getString(R.string.dialog_delete_msg, repoDir))
|
.setMessage(resources.getString(R.string.dialog_delete_msg, repoDir))
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.dialog_delete) { dialogInterface, _ ->
|
.setPositiveButton(R.string.dialog_delete) { dialogInterface, _ ->
|
||||||
try {
|
try {
|
||||||
FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext))
|
FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext))
|
||||||
PasswordRepository.closeRepository()
|
PasswordRepository.closeRepository()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//TODO Handle the diffent cases of exceptions
|
//TODO Handle the different cases of exceptions
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedPreferences.edit().putBoolean("repository_initialized", false).apply()
|
sharedPreferences.edit().putBoolean("repository_initialized", false).apply()
|
||||||
dialogInterface.cancel()
|
dialogInterface.cancel()
|
||||||
callingActivity.finish()
|
callingActivity.finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } }
|
.setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } }
|
||||||
.show()
|
.show()
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
val externalRepo = findPreference("pref_select_external")
|
val externalRepo = findPreference("pref_select_external")
|
||||||
externalRepo.summary =
|
externalRepo.summary =
|
||||||
sharedPreferences.getString("git_external_repo", callingActivity.getString(R.string.no_repo_selected))
|
sharedPreferences.getString("git_external_repo", callingActivity.getString(R.string.no_repo_selected))
|
||||||
externalRepo.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
externalRepo.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
callingActivity.selectExternalGitRepository()
|
callingActivity.selectExternalGitRepository()
|
||||||
true
|
true
|
||||||
|
@ -154,19 +148,22 @@ class UserPreference : AppCompatActivity() {
|
||||||
|
|
||||||
findPreference("autofill_enable").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
findPreference("autofill_enable").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
AlertDialog.Builder(callingActivity).setTitle(R.string.pref_autofill_enable_title)
|
AlertDialog.Builder(callingActivity).setTitle(R.string.pref_autofill_enable_title)
|
||||||
.setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ ->
|
.setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener {
|
}.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener {
|
||||||
(findPreference("autofill_enable") as CheckBoxPreference).isChecked =
|
(findPreference("autofill_enable") as CheckBoxPreference).isChecked =
|
||||||
(activity as UserPreference).isServiceEnabled
|
(activity as UserPreference).isServiceEnabled
|
||||||
}.show()
|
}.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference("export_passwords").onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
findPreference("export_passwords").apply {
|
||||||
callingActivity.exportPasswordsWithPermissions()
|
isEnabled = sharedPreferences.getBoolean("repository_initialized", false)
|
||||||
true
|
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
callingActivity.exportPasswords()
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference("general_show_time").onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
|
findPreference("general_show_time").onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
|
||||||
|
@ -184,23 +181,23 @@ class UserPreference : AppCompatActivity() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
val sharedPreferences = preferenceManager.sharedPreferences
|
val sharedPreferences = preferenceManager.sharedPreferences
|
||||||
findPreference("pref_select_external").summary =
|
findPreference("pref_select_external").summary =
|
||||||
preferenceManager.sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
|
preferenceManager.sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
|
||||||
findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false)
|
findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false)
|
||||||
findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false)
|
findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false)
|
||||||
findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString(
|
findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString(
|
||||||
"ssh_key_passphrase",
|
"ssh_key_passphrase",
|
||||||
null
|
null
|
||||||
)?.isNotEmpty() ?: false
|
)?.isNotEmpty() ?: false
|
||||||
findPreference("hotp_remember_clear_choice").isEnabled =
|
findPreference("hotp_remember_clear_choice").isEnabled =
|
||||||
sharedPreferences.getBoolean("hotp_remember_check", false)
|
sharedPreferences.getBoolean("hotp_remember_check", false)
|
||||||
findPreference("clear_after_copy").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
|
findPreference("clear_after_copy").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
|
||||||
findPreference("clear_clipboard_20x").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
|
findPreference("clear_clipboard_20x").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
|
||||||
val keyPref = findPreference("openpgp_key_id_pref")
|
val keyPref = findPreference("openpgp_key_id_pref")
|
||||||
val selectedKeys: Array<String> = ArrayList<String>(
|
val selectedKeys: Array<String> = ArrayList<String>(
|
||||||
sharedPreferences.getStringSet(
|
sharedPreferences.getStringSet(
|
||||||
"openpgp_key_ids_set",
|
"openpgp_key_ids_set",
|
||||||
HashSet<String>()
|
HashSet<String>()
|
||||||
)
|
)
|
||||||
).toTypedArray()
|
).toTypedArray()
|
||||||
if (selectedKeys.isEmpty()) {
|
if (selectedKeys.isEmpty()) {
|
||||||
keyPref.summary = this.resources.getString(R.string.pref_no_key_selected)
|
keyPref.summary = this.resources.getString(R.string.pref_no_key_selected)
|
||||||
|
@ -212,15 +209,14 @@ class UserPreference : AppCompatActivity() {
|
||||||
|
|
||||||
// see if the autofill service is enabled and check the preference accordingly
|
// see if the autofill service is enabled and check the preference accordingly
|
||||||
(findPreference("autofill_enable") as CheckBoxPreference).isChecked =
|
(findPreference("autofill_enable") as CheckBoxPreference).isChecked =
|
||||||
(activity as UserPreference).isServiceEnabled
|
(activity as UserPreference).isServiceEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
when (intent?.getStringExtra("operation")) {
|
when (intent?.getStringExtra("operation")) {
|
||||||
"get_ssh_key" -> getSshKeyWithPermissions(sharedPreferences.getBoolean("use_android_file_picker", false))
|
"get_ssh_key" -> getSshKey()
|
||||||
"make_ssh_key" -> makeSshKey(false)
|
"make_ssh_key" -> makeSshKey(false)
|
||||||
"git_external" -> selectExternalGitRepository()
|
"git_external" -> selectExternalGitRepository()
|
||||||
}
|
}
|
||||||
|
@ -232,128 +228,46 @@ class UserPreference : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectExternalGitRepository() {
|
fun selectExternalGitRepository() {
|
||||||
val activity = this
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(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))
|
||||||
.setPositiveButton(R.string.dialog_ok) { _, _ ->
|
.setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||||
// This always works
|
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
val i = Intent(activity.applicationContext, FilePickerActivity::class.java)
|
startActivityForResult(Intent.createChooser(i, "Choose Directory"), SELECT_GIT_DIRECTORY)
|
||||||
// This works if you defined the intent filter
|
}.setNegativeButton(R.string.dialog_cancel, null).show()
|
||||||
// Intent i = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
|
|
||||||
// Set these depending on your use case. These are the defaults.
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
|
||||||
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path)
|
|
||||||
|
|
||||||
startActivityForResult(i, SELECT_GIT_DIRECTORY)
|
|
||||||
}.setNegativeButton(R.string.dialog_cancel, null).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
// Handle action bar item clicks here. The action bar will
|
// Handle action bar item clicks here. The action bar will
|
||||||
// automatically handle clicks on the Home/Up button, so long
|
// automatically handle clicks on the Home/Up button, so long
|
||||||
// as you specify a parent activity in AndroidManifest.xml.
|
// as you specify a parent activity in AndroidManifest.xml.
|
||||||
val id = item.itemId
|
return when (item.itemId) {
|
||||||
when (id) {
|
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file explorer to import the private key
|
* Opens a file explorer to import the private key
|
||||||
*/
|
*/
|
||||||
fun getSshKeyWithPermissions(useDefaultPicker: Boolean) = runWithPermissions(
|
private fun getSshKey() {
|
||||||
requestedPermission = Manifest.permission.READ_EXTERNAL_STORAGE,
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
requestCode = REQUEST_EXTERNAL_STORAGE_SSH_KEY,
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
reason = "We need access to the sd-card to import the ssh-key"
|
type = "*/*"
|
||||||
) {
|
|
||||||
getSshKey(useDefaultPicker)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a file explorer to import the private key
|
|
||||||
*/
|
|
||||||
private fun getSshKey(useDefaultPicker: Boolean) {
|
|
||||||
val intent = if (useDefaultPicker) {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.setType("*/*")
|
|
||||||
} else {
|
|
||||||
// This always works
|
|
||||||
val intent = Intent(applicationContext, FilePickerActivity::class.java)
|
|
||||||
|
|
||||||
// Set these depending on your use case. These are the defaults.
|
|
||||||
intent.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
|
||||||
intent.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false)
|
|
||||||
intent.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE)
|
|
||||||
|
|
||||||
intent.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path)
|
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, IMPORT_SSH_KEY)
|
startActivityForResult(intent, IMPORT_SSH_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a function after checking that the permissions have been requested
|
|
||||||
*
|
|
||||||
* @param requestedPermission The permission to request
|
|
||||||
* @param requestCode The code passed to onRequestPermissionsResult
|
|
||||||
* @param reason The text to be shown to the user to explain why we're requesting this permission
|
|
||||||
* @param body The function to run
|
|
||||||
*/
|
|
||||||
private fun runWithPermissions(requestedPermission: String, requestCode: Int, reason: String, body: () -> Unit) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this, requestedPermission) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, requestedPermission)) {
|
|
||||||
val snack = Snackbar.make(prefsFragment.view, reason, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.setAction(R.string.dialog_ok) {
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(requestedPermission), requestCode)
|
|
||||||
}
|
|
||||||
snack.show()
|
|
||||||
val view = snack.view
|
|
||||||
val tv = view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
|
|
||||||
tv.setTextColor(Color.WHITE)
|
|
||||||
tv.maxLines = 10
|
|
||||||
} else {
|
|
||||||
// No explanation needed, we can request the permission.
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(requestedPermission), requestCode)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports the passwords after requesting permissions
|
|
||||||
*/
|
|
||||||
fun exportPasswordsWithPermissions() = runWithPermissions(
|
|
||||||
requestedPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
|
||||||
requestCode = REQUEST_EXTERNAL_STORAGE_SSH_KEY,
|
|
||||||
reason = "We need access to the sd-card to export the passwords"
|
|
||||||
) {
|
|
||||||
exportPasswords()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports the passwords
|
* Exports the passwords
|
||||||
*/
|
*/
|
||||||
private fun exportPasswords() {
|
private fun exportPasswords() {
|
||||||
val i = Intent(applicationContext, FilePickerActivity::class.java)
|
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
startActivityForResult(Intent.createChooser(i, "Choose Directory"), EXPORT_PASSWORDS)
|
||||||
// Set these depending on your use case. These are the defaults.
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
|
||||||
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path)
|
|
||||||
|
|
||||||
startActivityForResult(i, EXPORT_PASSWORDS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -370,11 +284,25 @@ class UserPreference : AppCompatActivity() {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun copySshKey(uri: Uri) {
|
private fun copySshKey(uri: Uri) {
|
||||||
val sshKey = this.contentResolver.openInputStream(uri)
|
// TODO: Check if valid SSH Key before import
|
||||||
if (sshKey != null) {
|
val sshKeyInputStream = contentResolver.openInputStream(uri)
|
||||||
val privateKey = IOUtils.toByteArray(sshKey)
|
if (sshKeyInputStream != null) {
|
||||||
FileUtils.writeByteArrayToFile(File(filesDir.toString() + "/.ssh_key"), privateKey)
|
|
||||||
sshKey.close()
|
val internalKeyFile = File("""$filesDir/.ssh_key""")
|
||||||
|
|
||||||
|
if (internalKeyFile.exists()) {
|
||||||
|
internalKeyFile.delete()
|
||||||
|
internalKeyFile.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
val sshKeyOutputSteam = internalKeyFile.outputStream()
|
||||||
|
|
||||||
|
sshKeyInputStream.copyTo(sshKeyOutputSteam, 1024)
|
||||||
|
|
||||||
|
sshKeyInputStream.close()
|
||||||
|
sshKeyOutputSteam.close()
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, getString(R.string.ssh_key_does_not_exist), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, getString(R.string.ssh_key_does_not_exist), Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
@ -384,7 +312,7 @@ class UserPreference : AppCompatActivity() {
|
||||||
private val isServiceEnabled: Boolean
|
private val isServiceEnabled: Boolean
|
||||||
get() {
|
get() {
|
||||||
val am = this
|
val am = this
|
||||||
.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||||
val runningServices = am
|
val runningServices = am
|
||||||
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC)
|
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC)
|
||||||
return runningServices
|
return runningServices
|
||||||
|
@ -393,8 +321,8 @@ class UserPreference : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(
|
override fun onActivityResult(
|
||||||
requestCode: Int, resultCode: Int,
|
requestCode: Int, resultCode: Int,
|
||||||
data: Intent?
|
data: Intent?
|
||||||
) {
|
) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
|
@ -408,28 +336,27 @@ class UserPreference : AppCompatActivity() {
|
||||||
val uri: Uri = data.data ?: throw IOException("Unable to open file")
|
val uri: Uri = data.data ?: throw IOException("Unable to open file")
|
||||||
|
|
||||||
copySshKey(uri)
|
copySshKey(uri)
|
||||||
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this,
|
this,
|
||||||
this.resources.getString(R.string.ssh_key_success_dialog_title),
|
this.resources.getString(R.string.ssh_key_success_dialog_title),
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
|
||||||
prefs.edit().putBoolean("use_generated_key", false).apply()
|
prefs.edit().putBoolean("use_generated_key", false).apply()
|
||||||
|
|
||||||
//delete the public key from generation
|
// Delete the public key from generation
|
||||||
val file = File(filesDir.toString() + "/.ssh_key.pub")
|
File("""$filesDir/.ssh_key.pub""").delete()
|
||||||
file.delete()
|
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title))
|
.setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title))
|
||||||
.setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message)
|
.setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message)
|
||||||
.setPositiveButton(this.resources.getString(R.string.dialog_ok)) { _, _ ->
|
.setPositiveButton(this.resources.getString(R.string.dialog_ok), null)
|
||||||
// pass
|
.show()
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EDIT_GIT_INFO -> {
|
EDIT_GIT_INFO -> {
|
||||||
|
@ -438,62 +365,114 @@ class UserPreference : AppCompatActivity() {
|
||||||
SELECT_GIT_DIRECTORY -> {
|
SELECT_GIT_DIRECTORY -> {
|
||||||
val uri = data.data
|
val uri = data.data
|
||||||
|
|
||||||
if (uri?.path == Environment.getExternalStorageDirectory().path) {
|
Log.d(TAG, "Selected repository URI is $uri")
|
||||||
// the user wants to use the root of the sdcard as a store...
|
// TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile
|
||||||
|
val docId = DocumentsContract.getTreeDocumentId(uri)
|
||||||
|
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
|
val repoPath = "${Environment.getExternalStorageDirectory()}/${split[1]}"
|
||||||
|
|
||||||
|
Log.d(TAG, "Selected repository path is $repoPath")
|
||||||
|
|
||||||
|
if (Environment.getExternalStorageDirectory().path == repoPath) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("SD-Card root selected")
|
.setTitle(getString(R.string.sdcard_root_warning_title))
|
||||||
.setMessage(
|
.setMessage(getString(R.string.sdcard_root_warning_message))
|
||||||
"You have selected the root of your sdcard for the store. " +
|
.setPositiveButton("Remove everything") { _, _ ->
|
||||||
"This is extremely dangerous and you will lose your data " +
|
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
"as its content will, eventually, be deleted"
|
.edit()
|
||||||
)
|
.putString("git_external_repo", uri?.path)
|
||||||
.setPositiveButton("Remove everything") { _, _ ->
|
.apply()
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
}.setNegativeButton(R.string.dialog_cancel, null).show()
|
||||||
.edit()
|
|
||||||
.putString("git_external_repo", uri?.path)
|
|
||||||
.apply()
|
|
||||||
}.setNegativeButton(R.string.dialog_cancel, null).show()
|
|
||||||
} else {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
|
||||||
.edit()
|
|
||||||
.putString("git_external_repo", uri?.path)
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
.edit()
|
||||||
|
.putString("git_external_repo", repoPath)
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
EXPORT_PASSWORDS -> {
|
EXPORT_PASSWORDS -> {
|
||||||
val uri = data.data
|
val uri = data.data
|
||||||
val repositoryDirectory = PasswordRepository.getRepositoryDirectory(applicationContext)
|
|
||||||
val fmtOut = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.US)
|
if (uri != null) {
|
||||||
val date = Date()
|
val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri)
|
||||||
val passwordNow = "/password_store_" + fmtOut.format(date)
|
|
||||||
val targetDirectory = File(uri?.path + passwordNow)
|
if (targetDirectory != null) {
|
||||||
if (repositoryDirectory != null) {
|
exportPasswords(targetDirectory)
|
||||||
try {
|
|
||||||
FileUtils.copyDirectory(repositoryDirectory, targetDirectory, true)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.d("PWD_EXPORT", "Exception happened : " + e.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
/**
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
|
* Exports passwords to the given directory.
|
||||||
when (requestCode) {
|
*
|
||||||
REQUEST_EXTERNAL_STORAGE_SSH_KEY -> {
|
* Recursively copies the existing password store to an external directory.
|
||||||
// If request is cancelled, the result arrays are empty.
|
*
|
||||||
if (grantResults.isNotEmpty() && PackageManager.PERMISSION_GRANTED in grantResults) {
|
* @param targetDirectory directory to copy password directory to.
|
||||||
getSshKey(sharedPreferences.getBoolean("use_android_file_picker", false))
|
*/
|
||||||
}
|
private fun exportPasswords(targetDirectory: DocumentFile) {
|
||||||
|
|
||||||
|
val repositoryDirectory = PasswordRepository.getRepositoryDirectory(applicationContext)
|
||||||
|
val sourcePassDir = DocumentFile.fromFile(repositoryDirectory)
|
||||||
|
|
||||||
|
Log.d(TAG, "Copying ${repositoryDirectory.path} to $targetDirectory")
|
||||||
|
|
||||||
|
val dateString = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
LocalDateTime
|
||||||
|
.now()
|
||||||
|
.format(DateTimeFormatter.ISO_DATE_TIME)
|
||||||
|
} else {
|
||||||
|
String.format("%tFT%<tRZ", Calendar.getInstance(TimeZone.getTimeZone("Z")))
|
||||||
|
}
|
||||||
|
|
||||||
|
val passDir = targetDirectory.createDirectory("password_store_$dateString")
|
||||||
|
|
||||||
|
if (passDir != null) {
|
||||||
|
copyDirToDir(sourcePassDir, passDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a password file to a given directory.
|
||||||
|
*
|
||||||
|
* Note: this does not preserve last modified time.
|
||||||
|
*
|
||||||
|
* @param passwordFile password file to copy.
|
||||||
|
* @param targetDirectory target directory to copy password.
|
||||||
|
*/
|
||||||
|
private fun copyFileToDir(passwordFile: DocumentFile, targetDirectory: DocumentFile) {
|
||||||
|
val sourceInputStream = contentResolver.openInputStream(passwordFile.uri)
|
||||||
|
val name = passwordFile.name
|
||||||
|
val targetPasswordFile = targetDirectory.createFile("application/octet-stream", name!!)
|
||||||
|
if (targetPasswordFile?.exists() == true) {
|
||||||
|
val destOutputStream = contentResolver.openOutputStream(targetPasswordFile.uri)
|
||||||
|
|
||||||
|
if (destOutputStream != null && sourceInputStream != null) {
|
||||||
|
sourceInputStream.copyTo(destOutputStream, 1024)
|
||||||
|
|
||||||
|
sourceInputStream.close()
|
||||||
|
destOutputStream.close()
|
||||||
}
|
}
|
||||||
REQUEST_EXTERNAL_STORAGE_EXPORT_PWD -> {
|
}
|
||||||
if (grantResults.isNotEmpty() && PackageManager.PERMISSION_GRANTED in grantResults) {
|
}
|
||||||
exportPasswords()
|
|
||||||
}
|
/**
|
||||||
|
* Recursively copies a directory to a destination.
|
||||||
|
*
|
||||||
|
* @param sourceDirectory directory to copy from.
|
||||||
|
* @param sourceDirectory directory to copy to.
|
||||||
|
*/
|
||||||
|
private fun copyDirToDir(sourceDirectory: DocumentFile, targetDirectory: DocumentFile) {
|
||||||
|
sourceDirectory.listFiles().forEach { file ->
|
||||||
|
if (file.isDirectory) {
|
||||||
|
// Create new directory and recurse
|
||||||
|
val newDir = targetDirectory.createDirectory(file.name!!)
|
||||||
|
copyDirToDir(file, newDir!!)
|
||||||
|
} else {
|
||||||
|
copyFileToDir(file, targetDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -505,7 +484,6 @@ class UserPreference : AppCompatActivity() {
|
||||||
private const val SELECT_GIT_DIRECTORY = 4
|
private const val SELECT_GIT_DIRECTORY = 4
|
||||||
private const val EXPORT_PASSWORDS = 5
|
private const val EXPORT_PASSWORDS = 5
|
||||||
private const val EDIT_GIT_CONFIG = 6
|
private const val EDIT_GIT_CONFIG = 6
|
||||||
private const val REQUEST_EXTERNAL_STORAGE_SSH_KEY = 50
|
private const val TAG = "UserPreference"
|
||||||
private const val REQUEST_EXTERNAL_STORAGE_EXPORT_PWD = 51
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -256,4 +256,6 @@
|
||||||
<string name="no_ssh_api_provider">No SSH API provider found. Is OpenKeychain installed?</string>
|
<string name="no_ssh_api_provider">No SSH API provider found. Is OpenKeychain installed?</string>
|
||||||
<string name="ssh_api_pending_intent_failed">SSH API pending intent failed</string>
|
<string name="ssh_api_pending_intent_failed">SSH API pending intent failed</string>
|
||||||
<string name="ssh_api_unknown_error">Unknown SSH API Error</string>
|
<string name="ssh_api_unknown_error">Unknown SSH API Error</string>
|
||||||
|
<string name="sdcard_root_warning_title">SD-Card root selected</string>
|
||||||
|
<string name="sdcard_root_warning_message">You have selected the root of your sdcard for the store. This is extremely dangerous and you will lose your data as its content will, eventually, be deleted</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -14,17 +14,6 @@
|
||||||
<item name="background">@color/blue_grey_700</item>
|
<item name="background">@color/blue_grey_700</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- You can also inherit from NNF_BaseTheme.Light -->
|
|
||||||
<style name="FilePickerTheme" parent="NNF_BaseTheme">
|
|
||||||
<!-- Set these to match your theme -->
|
|
||||||
<item name="colorPrimary">@color/blue_grey_500</item>
|
|
||||||
<item name="colorPrimaryDark">@color/blue_grey_700</item>
|
|
||||||
<item name="colorAccent">@color/teal_A700</item>
|
|
||||||
|
|
||||||
<!-- Need to set this also to style create folder dialog -->
|
|
||||||
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
|
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
|
||||||
<item name="colorPrimary">@color/blue_grey_500</item>
|
<item name="colorPrimary">@color/blue_grey_500</item>
|
||||||
<item name="colorPrimaryDark">@color/blue_grey_700</item>
|
<item name="colorPrimaryDark">@color/blue_grey_700</item>
|
||||||
|
|
|
@ -28,7 +28,7 @@ tasks.named<DependencyUpdatesTask>("dependencyUpdates") {
|
||||||
resolutionStrategy {
|
resolutionStrategy {
|
||||||
componentSelection {
|
componentSelection {
|
||||||
all {
|
all {
|
||||||
val blacklistedGroups = listOf("com.nononsenseapps", "commons-io", "org.eclipse.jgit")
|
val blacklistedGroups = listOf("commons-io", "org.eclipse.jgit")
|
||||||
val rejected = listOf("alpha", "beta", "rc", "cr", "m", "preview")
|
val rejected = listOf("alpha", "beta", "rc", "cr", "m", "preview")
|
||||||
.map { qualifier -> Regex("(?i).*[.-]$qualifier[.\\d-]*") }
|
.map { qualifier -> Regex("(?i).*[.-]$qualifier[.\\d-]*") }
|
||||||
.any { it.matches(candidate.version) && blacklistedGroups.contains(candidate.group) }
|
.any { it.matches(candidate.version) && blacklistedGroups.contains(candidate.group) }
|
||||||
|
|
Loading…
Reference in a new issue