Remove support for external storage and raise target SDK to 31 (#1863)

This commit is contained in:
Harsh Shandilya 2022-04-21 23:34:14 +05:30 committed by GitHub
parent 545da8f79b
commit 493e869022
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 161 additions and 517 deletions

View file

@ -40,6 +40,7 @@ All notable changes to this project will be documented in this file.
- Using HTTPS without authentication is now fully supported, and no longer asks for a username
- Enabling 'Show hidden files and folders' no longer shows Git-related files and folders
- XkPasswd password generator has been removed in favor of one backed by [Diceware](https://theworld.com/~reinhold/diceware.html)
- Support for stores outside the hidden app directory has been removed due to technical restrictions, see [this issue](https://msfjarvis.dev/aps/issue/1849) for details.
## [1.13.5] - 2021-07-28

View file

@ -8,10 +8,11 @@
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Required by Autofill to verify the certificate hashes of packages -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-feature
android:name="android.hardware.touchscreen"
@ -23,23 +24,28 @@
<application
android:name="dev.msfjarvis.aps.Application"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_content"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppThemeM3"
tools:ignore="GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="s">
<activity
android:name="dev.msfjarvis.aps.ui.passwords.PasswordStore"
android:configChanges="orientation|screenSize" />
android:configChanges="orientation|screenSize"
android:exported="false" />
<activity
android:name="dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity"
android:configChanges="orientation|screenSize" />
android:configChanges="orientation|screenSize"
android:exported="false" />
<activity
android:name="dev.msfjarvis.aps.ui.proxy.ProxySelectorActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
@ -60,6 +66,7 @@
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:clearTaskOnLaunch="true"
android:exported="false"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden"
@ -67,50 +74,56 @@
<activity
android:name="dev.msfjarvis.aps.ui.git.config.GitServerConfigActivity"
android:exported="false"
android:label="@string/title_activity_git_clone"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.git.config.GitConfigActivity"
android:exported="false"
android:label="@string/title_activity_git_config"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.git.log.GitLogActivity"
android:exported="false"
android:label="@string/title_activity_git_log" />
<activity
android:name="dev.msfjarvis.aps.ui.settings.SettingsActivity"
android:exported="false"
android:label="@string/action_settings"
android:parentActivityName=".ui.passwords.PasswordStore" />
<activity
android:name="dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity"
android:theme="@style/NoBackgroundThemeM3" />
<activity
android:name="dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity"
android:exported="false"
android:label="@string/new_password_title"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.crypto.PasswordCreationActivityV2"
android:exported="false"
android:label="@string/new_password_title"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.crypto.DecryptActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.crypto.GetKeyIdsActivity"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3" />
<service
android:name="dev.msfjarvis.aps.util.services.ClipboardService"
android:exported="false"
android:process=":clipboard_service_process" />
<service
android:name="dev.msfjarvis.aps.util.services.PasswordExportService"
android:exported="false"
android:process=":password_export_service_process" />
<service
android:name="dev.msfjarvis.aps.util.services.OreoAutofillService"
@ -124,41 +137,66 @@
android:resource="@xml/oreo_autofill_service" />
</service>
<activity android:name="dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity" />
<activity
android:name="dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity"
android:exported="false" />
<activity
android:name="dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity"
android:exported="false"
android:label="@string/pref_ssh_keygen_title"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="dev.msfjarvis.aps.ui.autofill.AutofillDecryptActivity"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3" />
<activity
android:name="dev.msfjarvis.aps.ui.autofill.AutofillDecryptActivityV2"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3" />
<activity
android:name="dev.msfjarvis.aps.ui.autofill.AutofillFilterView"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="dev.msfjarvis.aps.ui.autofill.AutofillSaveActivity"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3" />
<activity
android:name="dev.msfjarvis.aps.autofill.oreo.ui.AutofillSmsActivity"
android:configChanges="orientation"
android:exported="false"
android:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="dev.msfjarvis.aps.ui.autofill.AutofillPublisherChangedActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" />
<activity android:name="dev.msfjarvis.aps.ui.pgp.PGPKeyImportActivity"
<activity
android:name="dev.msfjarvis.aps.ui.pgp.PGPKeyImportActivity"
android:theme="@style/NoBackgroundThemeM3" />
</application>
<queries>
<package android:name="org.sufficientlysecure.keychain" />
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>

View file

@ -10,7 +10,6 @@ import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import dev.msfjarvis.aps.Application
import dev.msfjarvis.aps.data.password.PasswordItem
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.settings.PasswordSortOrder
@ -106,12 +105,7 @@ object PasswordRepository {
}
fun getRepositoryDirectory(): File {
return if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo != null) File(externalRepo) else File(filesDir.toString(), "/store")
} else {
File(filesDir.toString(), "/store")
}
return File(filesDir.toString(), "/store")
}
fun initialize(): Repository? {

View file

@ -11,7 +11,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.data.repo.PasswordRepository
import dev.msfjarvis.aps.databinding.FragmentCloneBinding
import dev.msfjarvis.aps.ui.git.config.GitServerConfigActivity
import dev.msfjarvis.aps.util.extensions.finish
@ -20,6 +23,9 @@ import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
class CloneFragment : Fragment(R.layout.fragment_clone) {
@ -38,17 +44,33 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.cloneRemote.setOnClickListener { cloneToHiddenDir() }
binding.createLocal.setOnClickListener {
parentFragmentManager.performTransactionWithBackStack(RepoLocationFragment.newInstance())
}
binding.createLocal.setOnClickListener { createRepository() }
}
/** Clones a remote Git repository to the app's private directory */
private fun cloneToHiddenDir() {
settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) }
cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext()))
}
private fun createRepository() {
val localDir = PasswordRepository.getRepositoryDirectory()
runCatching {
check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" }
PasswordRepository.createRepository(localDir)
if (!PasswordRepository.isInitialized) {
PasswordRepository.initialize()
}
parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance())
}
.onFailure { e ->
logcat(ERROR) { e.asLog() }
if (!localDir.delete()) {
logcat { "Failed to delete local repository: $localDir" }
}
finish()
}
}
companion object {
fun newInstance(): CloneFragment = CloneFragment()

View file

@ -1,198 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package dev.msfjarvis.aps.ui.onboarding.fragments
import android.Manifest
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.data.repo.PasswordRepository
import dev.msfjarvis.aps.databinding.FragmentRepoLocationBinding
import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
import dev.msfjarvis.aps.util.extensions.finish
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.isPermissionGranted
import dev.msfjarvis.aps.util.extensions.listFilesRecursively
import dev.msfjarvis.aps.util.extensions.performTransactionWithBackStack
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding
import dev.msfjarvis.aps.util.settings.PasswordSortOrder
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import java.io.File
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs }
private val directorySelectIntent by unsafeLazy {
Intent(requireContext(), DirectorySelectionActivity::class.java)
}
private val binding by viewBinding(FragmentRepoLocationBinding::bind)
private val sortOrder: PasswordSortOrder
get() = PasswordSortOrder.getSortOrder(settings)
private val repositoryInitAction =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == AppCompatActivity.RESULT_OK) {
initializeRepositoryInfo()
}
}
private val externalDirectorySelectAction =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == AppCompatActivity.RESULT_OK) {
if (checkExternalDirectory()) {
finish()
} else {
createRepository()
}
}
}
private val externalDirPermGrantedAction = createPermGrantedAction {
externalDirectorySelectAction.launch(directorySelectIntent)
}
private val repositoryUsePermGrantedAction = createPermGrantedAction {
initializeRepositoryInfo()
}
private val repositoryChangePermGrantedAction = createPermGrantedAction {
repositoryInitAction.launch(directorySelectIntent)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hidden.setOnClickListener { createRepoInHiddenDir() }
binding.sdcard.setOnClickListener { createRepoFromExternalDir() }
}
/** Initializes an empty repository in the app's private directory */
private fun createRepoInHiddenDir() {
settings.edit {
putBoolean(PreferenceKeys.GIT_EXTERNAL, false)
remove(PreferenceKeys.GIT_EXTERNAL_REPO)
}
initializeRepositoryInfo()
}
/** Initializes an empty repository in a selected directory if one does not already exist */
private fun createRepoFromExternalDir() {
settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) }
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo == null) {
if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
externalDirPermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
// Unlikely we have storage permissions without user ever selecting a directory,
// but let's not assume.
externalDirectorySelectAction.launch(directorySelectIntent)
}
} else {
MaterialAlertDialogBuilder(requireActivity())
.setTitle(resources.getString(R.string.directory_selected_title))
.setMessage(resources.getString(R.string.directory_selected_message, externalRepo))
.setPositiveButton(resources.getString(R.string.use)) { _, _ ->
if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
repositoryUsePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
initializeRepositoryInfo()
}
}
.setNegativeButton(resources.getString(R.string.change)) { _, _ ->
if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
repositoryChangePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
repositoryInitAction.launch(directorySelectIntent)
}
}
.show()
}
}
private fun checkExternalDirectory(): Boolean {
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) &&
settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null
) {
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
val dir = externalRepoPath?.let { File(it) }
if (dir != null && // The directory could be opened
dir.exists() && // The directory exists
dir.isDirectory && // The directory, is really a directory
dir.listFilesRecursively().isNotEmpty() && // The directory contains files
// The directory contains a non-zero number of password files
PasswordRepository.getPasswords(
dir,
PasswordRepository.getRepositoryDirectory(),
sortOrder
)
.isNotEmpty()
) {
PasswordRepository.closeRepository()
return true
}
}
return false
}
private fun createRepository() {
val localDir = PasswordRepository.getRepositoryDirectory()
runCatching {
check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" }
PasswordRepository.createRepository(localDir)
if (!PasswordRepository.isInitialized) {
PasswordRepository.initialize()
}
parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance())
}
.onFailure { e ->
logcat(ERROR) { e.asLog() }
if (!localDir.delete()) {
logcat { "Failed to delete local repository: $localDir" }
}
finish()
}
}
private fun initializeRepositoryInfo() {
val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo && !isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return
}
if (externalRepo && externalRepoPath != null) {
if (checkExternalDirectory()) {
finish()
return
}
}
createRepository()
}
private fun createPermGrantedAction(block: () -> Unit) =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
block.invoke()
}
}
companion object {
fun newInstance(): RepoLocationFragment = RepoLocationFragment()
}
}

View file

@ -4,7 +4,6 @@
*/
package dev.msfjarvis.aps.ui.passwords
import android.Manifest
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
@ -40,12 +39,10 @@ import dev.msfjarvis.aps.ui.crypto.DecryptActivity
import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2
import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivityV2
import dev.msfjarvis.aps.ui.dialogs.BasicBottomSheet
import dev.msfjarvis.aps.ui.dialogs.FolderCreationDialogFragment
import dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity
import dev.msfjarvis.aps.ui.git.base.BaseGitActivity
import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
import dev.msfjarvis.aps.ui.settings.SettingsActivity
import dev.msfjarvis.aps.util.autofill.AutofillMatcher
import dev.msfjarvis.aps.util.extensions.base64
@ -53,7 +50,6 @@ import dev.msfjarvis.aps.util.extensions.commitChange
import dev.msfjarvis.aps.util.extensions.contains
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.isInsideRepository
import dev.msfjarvis.aps.util.extensions.isPermissionGranted
import dev.msfjarvis.aps.util.extensions.listFilesRecursively
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.features.Feature
@ -206,16 +202,7 @@ class PasswordStore : BaseGitActivity() {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
// If user opens app with permission granted then revokes and returns,
// prevent attempt to create password list fragment
var savedInstance = savedInstanceState
if (savedInstanceState != null &&
(!settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) ||
!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE))
) {
savedInstance = null
}
super.onCreate(savedInstance)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pwdstore)
model.currentDir.observe(this) { dir ->
@ -233,11 +220,7 @@ class PasswordStore : BaseGitActivity() {
override fun onResume() {
super.onResume()
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
hasRequiredStoragePermissions()
} else {
checkLocalRepository()
}
checkLocalRepository()
if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false) && ::searchItem.isInitialized) {
if (!searchItem.isActionViewExpanded) {
searchItem.expandActionView()
@ -362,33 +345,9 @@ class PasswordStore : BaseGitActivity() {
)
}
/**
* Validates if storage permission is granted, and requests for it if not. The return value is
* true if the permission has been granted.
*/
private fun hasRequiredStoragePermissions(): Boolean {
return if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
BasicBottomSheet.Builder(this)
.setMessageRes(R.string.access_sdcard_text)
.setPositiveButtonClickListener(getString(R.string.snackbar_action_grant)) {
storagePermissionRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
.build()
.show(supportFragmentManager, "STORAGE_PERMISSION_MISSING")
false
} else {
checkLocalRepository()
true
}
}
private fun checkLocalRepository() {
val repo = PasswordRepository.initialize()
if (repo == null) {
directorySelectAction.launch(Intent(this, DirectorySelectionActivity::class.java))
} else {
checkLocalRepository(PasswordRepository.getRepositoryDirectory())
}
PasswordRepository.initialize()
checkLocalRepository(PasswordRepository.getRepositoryDirectory())
}
private fun checkLocalRepository(localDir: File?) {

View file

@ -1,59 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package dev.msfjarvis.aps.ui.settings
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import logcat.logcat
class DirectorySelectionActivity : AppCompatActivity() {
@Suppress("DEPRECATION")
private val directorySelectAction =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
if (uri == null) return@registerForActivityResult
logcat { "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 path = if (split.size > 1) split[1] else split[0]
val repoPath = "${Environment.getExternalStorageDirectory()}/$path"
val prefs = sharedPrefs
logcat { "Selected repository path is $repoPath" }
if (Environment.getExternalStorageDirectory().path == repoPath) {
MaterialAlertDialogBuilder(this)
.setTitle(resources.getString(R.string.sdcard_root_warning_title))
.setMessage(resources.getString(R.string.sdcard_root_warning_message))
.setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) {
_,
_ ->
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) }
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
}
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) }
setResult(RESULT_OK)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
directorySelectAction.launch(null)
}
}

View file

@ -23,7 +23,6 @@ import dagger.hilt.components.SingletonComponent
import de.Maxr1998.modernpreferences.Preference
import de.Maxr1998.modernpreferences.PreferenceScreen
import de.Maxr1998.modernpreferences.helpers.checkBox
import de.Maxr1998.modernpreferences.helpers.onCheckedChange
import de.Maxr1998.modernpreferences.helpers.onClick
import de.Maxr1998.modernpreferences.helpers.pref
import dev.msfjarvis.aps.R
@ -60,17 +59,6 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
private var showSshKeyPref: Preference? = null
private fun selectExternalGitRepository() {
MaterialAlertDialogBuilder(activity)
.setTitle(activity.resources.getString(R.string.external_repository_dialog_title))
.setMessage(activity.resources.getString(R.string.external_repository_dialog_text))
.setPositiveButton(R.string.dialog_ok) { _, _ ->
activity.launchActivity(DirectorySelectionActivity::class.java)
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
}
override fun provideSettings(builder: PreferenceScreen.Builder) {
val encryptedPreferences = hiltEntryPoint.encryptedPreferences()
val gitSettings = hiltEntryPoint.gitSettings()
@ -162,64 +150,35 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
true
}
}
val deleteRepoPref =
pref(PreferenceKeys.GIT_DELETE_REPO) {
titleRes = R.string.pref_git_delete_repo_title
summaryRes = R.string.pref_git_delete_repo_summary
visible = !activity.sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)
onClick {
val repoDir = PasswordRepository.getRepositoryDirectory()
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.pref_dialog_delete_title)
.setMessage(activity.getString(R.string.dialog_delete_msg, repoDir))
.setCancelable(false)
.setPositiveButton(R.string.dialog_delete) { dialogInterface, _ ->
runCatching {
PasswordRepository.getRepositoryDirectory().deleteRecursively()
PasswordRepository.closeRepository()
}
.onFailure { it.message?.let { message -> activity.snackbar(message = message) } }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
activity.getSystemService<ShortcutManager>()?.apply {
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
}
}
activity.sharedPrefs.edit {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
}
dialogInterface.cancel()
activity.finish()
}
.setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ ->
run { dialogInterface.cancel() }
}
.show()
true
}
}
checkBox(PreferenceKeys.GIT_EXTERNAL) {
titleRes = R.string.pref_external_repository_title
summaryRes = R.string.pref_external_repository_summary
onCheckedChange { checked ->
deleteRepoPref.visible = !checked
deleteRepoPref.requestRebind()
PasswordRepository.closeRepository()
activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPO_CHANGED, true) }
true
}
}
pref(PreferenceKeys.GIT_EXTERNAL_REPO) {
val externalRepo = activity.sharedPrefs.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo != null) {
summary = externalRepo
} else {
summaryRes = R.string.pref_select_external_repository_summary_no_repo_selected
}
titleRes = R.string.pref_select_external_repository_title
dependency = PreferenceKeys.GIT_EXTERNAL
pref(PreferenceKeys.GIT_DELETE_REPO) {
titleRes = R.string.pref_git_delete_repo_title
summaryRes = R.string.pref_git_delete_repo_summary
onClick {
selectExternalGitRepository()
val repoDir = PasswordRepository.getRepositoryDirectory()
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.pref_dialog_delete_title)
.setMessage(activity.getString(R.string.dialog_delete_msg, repoDir))
.setCancelable(false)
.setPositiveButton(R.string.dialog_delete) { dialogInterface, _ ->
runCatching {
PasswordRepository.getRepositoryDirectory().deleteRecursively()
PasswordRepository.closeRepository()
}
.onFailure { it.message?.let { message -> activity.snackbar(message = message) } }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
activity.getSystemService<ShortcutManager>()?.apply {
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
}
}
activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) }
dialogInterface.cancel()
activity.finish()
}
.setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ ->
run { dialogInterface.cancel() }
}
.show()
true
}
}

View file

@ -26,6 +26,7 @@ fun runMigrations(filesDirPath: String, sharedPrefs: SharedPreferences, gitSetti
migrateToSshKey(filesDirPath, sharedPrefs)
migrateToClipboardHistory(sharedPrefs)
migrateToDiceware(sharedPrefs)
removeExternalStorageProperties(sharedPrefs)
}
private fun migrateToGitUrlBasedConfig(sharedPrefs: SharedPreferences, gitSettings: GitSettings) {
@ -132,3 +133,18 @@ private fun migrateToDiceware(sharedPrefs: SharedPreferences) {
}
}
}
private fun removeExternalStorageProperties(prefs: SharedPreferences) {
logcat(TAG, INFO) { "Removing preferences related to external storage" }
prefs.edit {
if (prefs.contains(PreferenceKeys.GIT_EXTERNAL)) {
if (prefs.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
putBoolean(PreferenceKeys.GIT_EXTERNAL_MIGRATED, true)
}
remove(PreferenceKeys.GIT_EXTERNAL)
}
if (prefs.contains(PreferenceKeys.GIT_EXTERNAL_REPO)) {
remove(PreferenceKeys.GIT_EXTERNAL_REPO)
}
}
}

View file

@ -8,7 +8,6 @@ package dev.msfjarvis.aps.util.settings
object PreferenceKeys {
const val APP_THEME = "app_theme"
const val APP_VERSION = "app_version"
const val AUTOFILL_ENABLE = "autofill_enable"
const val BIOMETRIC_AUTH = "biometric_auth"
@Deprecated(
@ -26,8 +25,11 @@ object PreferenceKeys {
const val GIT_CONFIG = "git_config"
const val GIT_CONFIG_AUTHOR_EMAIL = "git_config_user_email"
const val GIT_CONFIG_AUTHOR_NAME = "git_config_user_name"
@Deprecated(message = "We're removing support for external storage")
const val GIT_EXTERNAL = "git_external"
@Deprecated(message = "We're removing support for external storage")
const val GIT_EXTERNAL_REPO = "git_external_repo"
const val GIT_EXTERNAL_MIGRATED = "git_external_migrated"
const val GIT_REMOTE_AUTH = "git_remote_auth"
const val GIT_REMOTE_KEY_TYPE = "git_remote_key_type"
@ -50,10 +52,7 @@ object PreferenceKeys {
const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes"
const val OREO_AUTOFILL_DEFAULT_USERNAME = "oreo_autofill_default_username"
const val OREO_AUTOFILL_DIRECTORY_STRUCTURE = "oreo_autofill_directory_structure"
const val PREF_KEY_CUSTOM_DICT = "pref_key_custom_dict"
const val PREF_KEY_IS_CUSTOM_DICT = "pref_key_is_custom_dict"
const val PREF_KEY_PWGEN_TYPE = "pref_key_pwgen_type"
const val PREF_SELECT_EXTERNAL = "pref_select_external"
const val REPOSITORY_INITIALIZED = "repository_initialized"
const val REPO_CHANGED = "repo_changed"
const val SEARCH_ON_START = "search_on_start"

View file

@ -1,95 +0,0 @@
<!--
~ Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
~ SPDX-License-Identifier: GPL-3.0-only
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/app_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_icon_margin_top"
android:contentDescription="@string/app_icon_hint"
android:src="@mipmap/ic_launcher"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/fab_compat_margin"
android:layout_marginEnd="@dimen/fab_compat_margin"
android:text="@string/app_name"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/app_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/app_icon"
app:layout_constraintTop_toTopOf="@+id/app_icon" />
<TextView
android:id="@+id/repo_location"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginEnd="@dimen/fab_compat_margin"
android:text="@string/repository_n_location"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/app_icon"
app:layout_constraintTop_toBottomOf="@id/app_icon" />
<TextView
android:id="@+id/repo_location_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/onboarding_desc_margin_top"
android:layout_marginEnd="16dp"
android:text="@string/location_dialog_create_text"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/repo_location"
app:layout_constraintTop_toBottomOf="@id/repo_location" />
<com.google.android.material.button.MaterialButton
android:id="@+id/hidden"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="@dimen/onboarding_button_margin_top"
android:layout_marginEnd="16dp"
android:maxWidth="300dp"
android:minWidth="100dp"
android:text="@string/location_hidden"
app:layout_constraintBottom_toTopOf="@id/sdcard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/repo_location_text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sdcard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="128dp"
android:maxWidth="300dp"
android:minWidth="100dp"
android:text="@string/location_sdcard"
app:layout_constraintEnd_toEndOf="@id/hidden"
app:layout_constraintStart_toStartOf="@id/hidden"
app:layout_constraintTop_toBottomOf="@id/hidden" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content />

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup />
<device-transfer />
</data-extraction-rules>

View file

@ -83,7 +83,7 @@ class AutofillSmsActivity : AppCompatActivity() {
context,
fillOtpFromSmsRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
.intentSender
}

View file

@ -16,6 +16,7 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.Rule
import org.junit.rules.TemporaryFolder
@ -50,7 +51,6 @@ class MigrationsTest {
@Test
fun verifySshWithCustomPortMigration() {
sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_PORT, "2200")
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
@ -73,7 +73,6 @@ class MigrationsTest {
@Test
fun verifySshWithDefaultPortMigration() {
sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
@ -95,7 +94,6 @@ class MigrationsTest {
@Test
fun verifyHttpsWithGitHubMigration() {
sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "Android-Password-Store/pass-test")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "github.com")
@ -116,7 +114,6 @@ class MigrationsTest {
@Test
fun verifyHiddenFoldersMigrationIfDisabled() {
sharedPrefs.edit { clear() }
runMigrations(
filesDir,
sharedPrefs,
@ -128,10 +125,7 @@ class MigrationsTest {
@Test
fun verifyHiddenFoldersMigrationIfEnabled() {
sharedPrefs.edit {
clear()
putBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, true)
}
sharedPrefs.edit { putBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, true) }
runMigrations(
filesDir,
sharedPrefs,
@ -143,10 +137,7 @@ class MigrationsTest {
@Test
fun verifyClearClipboardHistoryMigration() {
sharedPrefs.edit {
clear()
putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true)
}
sharedPrefs.edit { putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true) }
runMigrations(
filesDir,
sharedPrefs,
@ -158,10 +149,7 @@ class MigrationsTest {
@Test
fun verifyClassicPasswordGeneratorMigration() {
sharedPrefs.edit {
clear()
putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "classic")
}
sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "classic") }
runMigrations(
filesDir,
sharedPrefs,
@ -172,10 +160,7 @@ class MigrationsTest {
@Test
fun verifyXkPasswdPasswordGeneratorMigration() {
sharedPrefs.edit {
clear()
putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "xkpasswd")
}
sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "xkpasswd") }
runMigrations(
filesDir,
sharedPrefs,
@ -183,4 +168,20 @@ class MigrationsTest {
)
assertEquals("diceware", sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE))
}
@Test
fun verifyExternalStorageMigration() {
sharedPrefs.edit {
putBoolean(PreferenceKeys.GIT_EXTERNAL, true)
putString(PreferenceKeys.GIT_EXTERNAL_REPO, "/sdcard/")
}
runMigrations(
filesDir,
sharedPrefs,
GitSettings(sharedPrefs, encryptedSharedPreferences, proxySharedPreferences, filesDir),
)
assertFalse { sharedPrefs.contains(PreferenceKeys.GIT_EXTERNAL) }
assertFalse { sharedPrefs.contains(PreferenceKeys.GIT_EXTERNAL_REPO) }
assertTrue { sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL_MIGRATED, false) }
}
}

View file

@ -244,7 +244,7 @@ private fun getBrowserAutofillSupportLevel(
public fun getInstalledBrowsersWithAutofillSupportLevel(
context: Context
): List<Pair<String, BrowserAutofillSupportLevel>> {
val testWebIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("http://example.org") }
val testWebIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("https://example.org") }
val installedBrowsers =
context.packageManager.queryIntentActivities(testWebIntent, PackageManager.MATCH_ALL)
return installedBrowsers

View file

@ -11,7 +11,7 @@ extensions.configure<TestedExtension> {
setCompileSdkVersion(31)
defaultConfig {
minSdk = 23
targetSdk = 29
targetSdk = 31
}
sourceSets {