diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2022-04-21 23:34:14 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-21 23:34:14 +0530 |
commit | 493e86902277d9e8b7996308aa589013b4418dd6 (patch) | |
tree | a421f91c8fa562a98763c2dd3c495ec3c2b51b8a | |
parent | 545da8f79bda905a5cad64fcf3012fdcdabf83b0 (diff) |
Remove support for external storage and raise target SDK to 31 (#1863)
17 files changed, 158 insertions, 514 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4142ce74..1c89d1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce40be47..aed314aa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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> diff --git a/app/src/main/java/dev/msfjarvis/aps/data/repo/PasswordRepository.kt b/app/src/main/java/dev/msfjarvis/aps/data/repo/PasswordRepository.kt index a3cf2dda..9bf1bce6 100644 --- a/app/src/main/java/dev/msfjarvis/aps/data/repo/PasswordRepository.kt +++ b/app/src/main/java/dev/msfjarvis/aps/data/repo/PasswordRepository.kt @@ -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? { diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt index 603c016b..16c1a784 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt @@ -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() diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt deleted file mode 100644 index befe44f1..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt index d80959b8..e4ec2359 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt @@ -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?) { diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt deleted file mode 100644 index 1f8873c0..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt index cbd9c18f..1c43127a 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt @@ -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) } } + pref(PreferenceKeys.GIT_DELETE_REPO) { + titleRes = R.string.pref_git_delete_repo_title + summaryRes = R.string.pref_git_delete_repo_summary + 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()) - } + 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 - onClick { - selectExternalGitRepository() + 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 } } diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt index 5d61e75b..b90856b1 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt @@ -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) + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt index 6168cfee..152275d6 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt @@ -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" diff --git a/app/src/main/res/layout/fragment_repo_location.xml b/app/src/main/res/layout/fragment_repo_location.xml deleted file mode 100644 index 0302eaff..00000000 --- a/app/src/main/res/layout/fragment_repo_location.xml +++ /dev/null @@ -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> diff --git a/app/src/main/res/xml/backup_content.xml b/app/src/main/res/xml/backup_content.xml new file mode 100644 index 00000000..1b0854f7 --- /dev/null +++ b/app/src/main/res/xml/backup_content.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<full-backup-content /> diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 00000000..2148d1dd --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<data-extraction-rules> + <cloud-backup /> + <device-transfer /> +</data-extraction-rules> diff --git a/app/src/nonFree/java/dev/msfjarvis/aps/autofill/oreo/ui/AutofillSmsActivity.kt b/app/src/nonFree/java/dev/msfjarvis/aps/autofill/oreo/ui/AutofillSmsActivity.kt index abe855cd..4c751bb0 100644 --- a/app/src/nonFree/java/dev/msfjarvis/aps/autofill/oreo/ui/AutofillSmsActivity.kt +++ b/app/src/nonFree/java/dev/msfjarvis/aps/autofill/oreo/ui/AutofillSmsActivity.kt @@ -83,7 +83,7 @@ class AutofillSmsActivity : AppCompatActivity() { context, fillOtpFromSmsRequestCode++, intent, - PendingIntent.FLAG_CANCEL_CURRENT + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) .intentSender } diff --git a/app/src/test/java/dev/msfjarvis/aps/util/settings/MigrationsTest.kt b/app/src/test/java/dev/msfjarvis/aps/util/settings/MigrationsTest.kt index d279ab03..cf89d81e 100644 --- a/app/src/test/java/dev/msfjarvis/aps/util/settings/MigrationsTest.kt +++ b/app/src/test/java/dev/msfjarvis/aps/util/settings/MigrationsTest.kt @@ -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,15 +160,28 @@ class MigrationsTest { @Test fun verifyXkPasswdPasswordGeneratorMigration() { + sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "xkpasswd") } + runMigrations( + filesDir, + sharedPrefs, + GitSettings(sharedPrefs, encryptedSharedPreferences, proxySharedPreferences, filesDir), + ) + assertEquals("diceware", sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE)) + } + + @Test + fun verifyExternalStorageMigration() { sharedPrefs.edit { - clear() - putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "xkpasswd") + putBoolean(PreferenceKeys.GIT_EXTERNAL, true) + putString(PreferenceKeys.GIT_EXTERNAL_REPO, "/sdcard/") } runMigrations( filesDir, sharedPrefs, GitSettings(sharedPrefs, encryptedSharedPreferences, proxySharedPreferences, filesDir), ) - assertEquals("diceware", sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE)) + assertFalse { sharedPrefs.contains(PreferenceKeys.GIT_EXTERNAL) } + assertFalse { sharedPrefs.contains(PreferenceKeys.GIT_EXTERNAL_REPO) } + assertTrue { sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL_MIGRATED, false) } } } diff --git a/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt index bd84746b..849113cf 100644 --- a/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt +++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt @@ -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 diff --git a/build-logic/android-plugins/src/main/kotlin/com.github.android-password-store.android-common.gradle.kts b/build-logic/android-plugins/src/main/kotlin/com.github.android-password-store.android-common.gradle.kts index 9400acc2..c7bbf0fd 100644 --- a/build-logic/android-plugins/src/main/kotlin/com.github.android-password-store.android-common.gradle.kts +++ b/build-logic/android-plugins/src/main/kotlin/com.github.android-password-store.android-common.gradle.kts @@ -11,7 +11,7 @@ extensions.configure<TestedExtension> { setCompileSdkVersion(31) defaultConfig { minSdk = 23 - targetSdk = 29 + targetSdk = 31 } sourceSets { |