diff options
author | Hussein Al Abry <zidhussein@gmail.com> | 2019-04-25 04:41:32 +0100 |
---|---|---|
committer | Harsh Shandilya <msfjarvis@gmail.com> | 2019-04-25 09:11:32 +0530 |
commit | e54010906f3cfc91ed41a044d44da548a60c9b0f (patch) | |
tree | 5b23598939803c015982bc892009c82849b799db | |
parent | f272e4dde2c05de64681a0e089387ef9c54bd7c7 (diff) |
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>
-rw-r--r-- | app/build.gradle.kts | 1 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 9 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.kt | 422 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 2 | ||||
-rw-r--r-- | app/src/main/res/values/styles.xml | 11 | ||||
-rw-r--r-- | build.gradle.kts | 2 |
6 files changed, 203 insertions, 244 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a82de5dc..4c90a102 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,7 +74,6 @@ dependencies { implementation("com.google.android.material:material:1.0.0") implementation("androidx.annotation:annotation:1.0.2") 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") { exclude(group = "org.apache.httpcomponents", module = "httpclient") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1a6338e3..ab55c835 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,15 +68,6 @@ <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.zeapo.pwdstore.PasswordStore" /> </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" android:parentActivityName=".PasswordStore"/> <activity android:name=".SelectFolderActivity" /> diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 7d1be72c..ebeb0ec5 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -1,46 +1,40 @@ package com.zeapo.pwdstore -import android.Manifest import android.accessibilityservice.AccessibilityServiceInfo import android.app.Activity import android.content.Context import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Color import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Environment import android.preference.CheckBoxPreference import android.preference.Preference import android.preference.PreferenceFragment import android.preference.PreferenceManager +import android.provider.DocumentsContract import android.provider.Settings import android.util.Log import android.view.MenuItem import android.view.accessibility.AccessibilityManager -import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.google.android.material.snackbar.Snackbar -import com.nononsenseapps.filepicker.FilePickerActivity +import androidx.documentfile.provider.DocumentFile import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity import com.zeapo.pwdstore.crypto.PgpActivity import com.zeapo.pwdstore.git.GitActivity import com.zeapo.pwdstore.utils.PasswordRepository import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils import org.openintents.openpgp.util.OpenPgpUtils import java.io.File import java.io.IOException -import java.text.SimpleDateFormat -import java.util.ArrayList -import java.util.Date -import java.util.Locale +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.* class UserPreference : AppCompatActivity() { + private lateinit var prefsFragment: PrefsFragment class PrefsFragment : PreferenceFragment() { @@ -61,7 +55,7 @@ class UserPreference : AppCompatActivity() { } findPreference("ssh_key").onPreferenceClickListener = Preference.OnPreferenceClickListener { - callingActivity.getSshKeyWithPermissions(sharedPreferences.getBoolean("use_android_file_picker", false)) + callingActivity.getSshKey() true } @@ -77,18 +71,18 @@ class UserPreference : AppCompatActivity() { } findPreference("ssh_key_clear_passphrase").onPreferenceClickListener = - Preference.OnPreferenceClickListener { - sharedPreferences.edit().putString("ssh_key_passphrase", null).apply() - it.isEnabled = false - true - } + Preference.OnPreferenceClickListener { + sharedPreferences.edit().putString("ssh_key_passphrase", null).apply() + it.isEnabled = false + true + } findPreference("hotp_remember_clear_choice").onPreferenceClickListener = - Preference.OnPreferenceClickListener { - sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply() - it.isEnabled = false - true - } + Preference.OnPreferenceClickListener { + sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply() + it.isEnabled = false + true + } findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener { val intent = Intent(callingActivity, GitActivity::class.java) @@ -107,30 +101,30 @@ class UserPreference : AppCompatActivity() { findPreference("git_delete_repo").onPreferenceClickListener = Preference.OnPreferenceClickListener { val repoDir = PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext) AlertDialog.Builder(callingActivity) - .setTitle(R.string.pref_dialog_delete_title) - .setMessage(resources.getString(R.string.dialog_delete_msg, repoDir)) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> - try { - FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)) - PasswordRepository.closeRepository() - } catch (e: Exception) { - //TODO Handle the diffent cases of exceptions + .setTitle(R.string.pref_dialog_delete_title) + .setMessage(resources.getString(R.string.dialog_delete_msg, repoDir)) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> + try { + FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)) + PasswordRepository.closeRepository() + } catch (e: Exception) { + //TODO Handle the different cases of exceptions + } + + sharedPreferences.edit().putBoolean("repository_initialized", false).apply() + dialogInterface.cancel() + callingActivity.finish() } - - sharedPreferences.edit().putBoolean("repository_initialized", false).apply() - dialogInterface.cancel() - callingActivity.finish() - } - .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } } - .show() + .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } } + .show() true } val externalRepo = findPreference("pref_select_external") 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 { callingActivity.selectExternalGitRepository() true @@ -154,19 +148,22 @@ class UserPreference : AppCompatActivity() { findPreference("autofill_enable").onPreferenceClickListener = Preference.OnPreferenceClickListener { AlertDialog.Builder(callingActivity).setTitle(R.string.pref_autofill_enable_title) - .setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ -> - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - startActivity(intent) - }.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener { - (findPreference("autofill_enable") as CheckBoxPreference).isChecked = - (activity as UserPreference).isServiceEnabled - }.show() + .setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ -> + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + startActivity(intent) + }.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener { + (findPreference("autofill_enable") as CheckBoxPreference).isChecked = + (activity as UserPreference).isServiceEnabled + }.show() true } - findPreference("export_passwords").onPreferenceClickListener = Preference.OnPreferenceClickListener { - callingActivity.exportPasswordsWithPermissions() - true + findPreference("export_passwords").apply { + isEnabled = sharedPreferences.getBoolean("repository_initialized", false) + onPreferenceClickListener = Preference.OnPreferenceClickListener { + callingActivity.exportPasswords() + true + } } findPreference("general_show_time").onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> @@ -184,23 +181,23 @@ class UserPreference : AppCompatActivity() { super.onStart() val sharedPreferences = preferenceManager.sharedPreferences 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("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false) findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString( - "ssh_key_passphrase", - null + "ssh_key_passphrase", + null )?.isNotEmpty() ?: false 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_clipboard_20x").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 val keyPref = findPreference("openpgp_key_id_pref") val selectedKeys: Array<String> = ArrayList<String>( - sharedPreferences.getStringSet( - "openpgp_key_ids_set", - HashSet<String>() - ) + sharedPreferences.getStringSet( + "openpgp_key_ids_set", + HashSet<String>() + ) ).toTypedArray() if (selectedKeys.isEmpty()) { 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 (findPreference("autofill_enable") as CheckBoxPreference).isChecked = - (activity as UserPreference).isServiceEnabled + (activity as UserPreference).isServiceEnabled } } public override fun onCreate(savedInstanceState: Bundle?) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.applicationContext) super.onCreate(savedInstanceState) when (intent?.getStringExtra("operation")) { - "get_ssh_key" -> getSshKeyWithPermissions(sharedPreferences.getBoolean("use_android_file_picker", false)) + "get_ssh_key" -> getSshKey() "make_ssh_key" -> makeSshKey(false) "git_external" -> selectExternalGitRepository() } @@ -232,128 +228,46 @@ class UserPreference : AppCompatActivity() { } fun selectExternalGitRepository() { - val activity = this AlertDialog.Builder(this) - .setTitle(this.resources.getString(R.string.external_repository_dialog_title)) - .setMessage(this.resources.getString(R.string.external_repository_dialog_text)) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - // This always works - val i = Intent(activity.applicationContext, FilePickerActivity::class.java) - // This works if you defined the intent filter - // 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() + .setTitle(this.resources.getString(R.string.external_repository_dialog_title)) + .setMessage(this.resources.getString(R.string.external_repository_dialog_text)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(Intent.createChooser(i, "Choose Directory"), SELECT_GIT_DIRECTORY) + }.setNegativeButton(R.string.dialog_cancel, null).show() } override fun onOptionsItemSelected(item: MenuItem): Boolean { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. - val id = item.itemId - when (id) { + return when (item.itemId) { android.R.id.home -> { setResult(Activity.RESULT_OK) finish() - return true + true } + else -> super.onOptionsItemSelected(item) } - return super.onOptionsItemSelected(item) } /** * Opens a file explorer to import the private key */ - fun getSshKeyWithPermissions(useDefaultPicker: Boolean) = runWithPermissions( - requestedPermission = Manifest.permission.READ_EXTERNAL_STORAGE, - requestCode = REQUEST_EXTERNAL_STORAGE_SSH_KEY, - reason = "We need access to the sd-card to import the ssh-key" - ) { - 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) + private fun getSshKey() { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" } 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 */ private fun exportPasswords() { - val i = Intent(applicationContext, FilePickerActivity::class.java) - - // 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) + val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(Intent.createChooser(i, "Choose Directory"), EXPORT_PASSWORDS) } /** @@ -370,11 +284,25 @@ class UserPreference : AppCompatActivity() { @Throws(IOException::class) private fun copySshKey(uri: Uri) { - val sshKey = this.contentResolver.openInputStream(uri) - if (sshKey != null) { - val privateKey = IOUtils.toByteArray(sshKey) - FileUtils.writeByteArrayToFile(File(filesDir.toString() + "/.ssh_key"), privateKey) - sshKey.close() + // TODO: Check if valid SSH Key before import + val sshKeyInputStream = contentResolver.openInputStream(uri) + if (sshKeyInputStream != null) { + + 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 { 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 get() { val am = this - .getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + .getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager val runningServices = am .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC) return runningServices @@ -393,8 +321,8 @@ class UserPreference : AppCompatActivity() { } override fun onActivityResult( - requestCode: Int, resultCode: Int, - data: Intent? + requestCode: Int, resultCode: Int, + data: Intent? ) { if (resultCode == Activity.RESULT_OK) { if (data == null) { @@ -408,28 +336,27 @@ class UserPreference : AppCompatActivity() { val uri: Uri = data.data ?: throw IOException("Unable to open file") copySshKey(uri) + Toast.makeText( - this, - this.resources.getString(R.string.ssh_key_success_dialog_title), - Toast.LENGTH_LONG + this, + this.resources.getString(R.string.ssh_key_success_dialog_title), + Toast.LENGTH_LONG ).show() val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) prefs.edit().putBoolean("use_generated_key", false).apply() - //delete the public key from generation - val file = File(filesDir.toString() + "/.ssh_key.pub") - file.delete() + // Delete the public key from generation + File("""$filesDir/.ssh_key.pub""").delete() setResult(Activity.RESULT_OK) finish() } catch (e: IOException) { AlertDialog.Builder(this) - .setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title)) - .setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message) - .setPositiveButton(this.resources.getString(R.string.dialog_ok)) { _, _ -> - // pass - }.show() + .setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title)) + .setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message) + .setPositiveButton(this.resources.getString(R.string.dialog_ok), null) + .show() } } EDIT_GIT_INFO -> { @@ -438,62 +365,114 @@ class UserPreference : AppCompatActivity() { SELECT_GIT_DIRECTORY -> { val uri = data.data - if (uri?.path == Environment.getExternalStorageDirectory().path) { - // the user wants to use the root of the sdcard as a store... + Log.d(TAG, "Selected repository URI is $uri") + // 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) - .setTitle("SD-Card root selected") - .setMessage( - "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" - ) - .setPositiveButton("Remove everything") { _, _ -> - PreferenceManager.getDefaultSharedPreferences(applicationContext) - .edit() - .putString("git_external_repo", uri?.path) - .apply() - }.setNegativeButton(R.string.dialog_cancel, null).show() - } else { - PreferenceManager.getDefaultSharedPreferences(applicationContext) + .setTitle(getString(R.string.sdcard_root_warning_title)) + .setMessage(getString(R.string.sdcard_root_warning_message)) + .setPositiveButton("Remove everything") { _, _ -> + PreferenceManager.getDefaultSharedPreferences(applicationContext) + .edit() + .putString("git_external_repo", uri?.path) + .apply() + }.setNegativeButton(R.string.dialog_cancel, null).show() + } + + PreferenceManager.getDefaultSharedPreferences(applicationContext) .edit() - .putString("git_external_repo", uri?.path) + .putString("git_external_repo", repoPath) .apply() - } } EXPORT_PASSWORDS -> { val uri = data.data - val repositoryDirectory = PasswordRepository.getRepositoryDirectory(applicationContext) - val fmtOut = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.US) - val date = Date() - val passwordNow = "/password_store_" + fmtOut.format(date) - val targetDirectory = File(uri?.path + passwordNow) - if (repositoryDirectory != null) { - try { - FileUtils.copyDirectory(repositoryDirectory, targetDirectory, true) - } catch (e: IOException) { - Log.d("PWD_EXPORT", "Exception happened : " + e.message) + + if (uri != null) { + val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri) + + if (targetDirectory != null) { + exportPasswords(targetDirectory) } } } - else -> { - } } } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.applicationContext) - when (requestCode) { - REQUEST_EXTERNAL_STORAGE_SSH_KEY -> { - // If request is cancelled, the result arrays are empty. - if (grantResults.isNotEmpty() && PackageManager.PERMISSION_GRANTED in grantResults) { - getSshKey(sharedPreferences.getBoolean("use_android_file_picker", false)) - } + /** + * Exports passwords to the given directory. + * + * Recursively copies the existing password store to an external directory. + * + * @param targetDirectory directory to copy password directory to. + */ + 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 EXPORT_PASSWORDS = 5 private const val EDIT_GIT_CONFIG = 6 - private const val REQUEST_EXTERNAL_STORAGE_SSH_KEY = 50 - private const val REQUEST_EXTERNAL_STORAGE_EXPORT_PWD = 51 + private const val TAG = "UserPreference" } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc4888ca..675c5ca9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -256,4 +256,6 @@ <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_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> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fc592058..0b0bc4e6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -14,17 +14,6 @@ <item name="background">@color/blue_grey_700</item> </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"> <item name="colorPrimary">@color/blue_grey_500</item> <item name="colorPrimaryDark">@color/blue_grey_700</item> diff --git a/build.gradle.kts b/build.gradle.kts index a82668b8..78fa58db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ tasks.named<DependencyUpdatesTask>("dependencyUpdates") { resolutionStrategy { componentSelection { 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") .map { qualifier -> Regex("(?i).*[.-]$qualifier[.\\d-]*") } .any { it.matches(candidate.version) && blacklistedGroups.contains(candidate.group) } |