feat: raise min SDK to 26

Autofill is only available on API 26 and above and I do not want to deal with
bugs on these older Android releases.
This commit is contained in:
Harsh Shandilya 2023-05-12 20:28:36 +05:30
parent ee9d77eafd
commit ec696c1d8d
No known key found for this signature in database
27 changed files with 63 additions and 164 deletions

View file

@ -35,7 +35,8 @@ All notable changes to this project will be documented in this file.
- **BREAKING**: The app's package name has been changed to `app.passwordstore` so users are aware that this is a new project with no compatibility guarantees with Password Store 1.x.y.
- **BREAKING**: Introduce a new PGP backend powered by [PGPainless](https://github.com/pgpainless/pgpainless) which completely replaces OpenKeychain
- **BREAKING**: Accessibility autofill has been removed completely due to being buggy, insecure and lacking in features. Upgrade to Android 8 or preferably later to gain access to our advanced Autofill implementation.
- **BREAKING***: Support for stores outside the hidden app directory has been removed due to technical restrictions, see [this issue](https://github.com/Android-Password-Store/Android-Password-Store/issues/1849) for details.
- **BREAKING**: Support for stores outside the hidden app directory has been removed due to technical restrictions, see [this issue](https://github.com/Android-Password-Store/Android-Password-Store/issues/1849) for details.
- **BREAKING**: The app's minimum supported Android version has been raised to Android Oreo (API level 26).
- The settings UI has been completely re-done to dramatically improve discoverability and navigation for users
- Using the `git://` protocol in the server URL now presents an explicit discouragement rather than a generic error
- Encrypted data is no longer ASCII armored, bringing it in line with `pass`

View file

@ -6,11 +6,8 @@ package app.passwordstore.autofill.oreo.ui
import android.content.Context
import android.content.IntentSender
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
@RequiresApi(Build.VERSION_CODES.O)
@Suppress("UNUSED_PARAMETER")
class AutofillSmsActivity : AppCompatActivity() {

View file

@ -96,9 +96,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.detectContentUriWithoutPermission()
}
builder.detectContentUriWithoutPermission()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.detectCredentialProtectedWhileLocked().detectImplicitDirectBoot()

View file

@ -11,7 +11,6 @@ import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.view.autofill.AutofillManager
import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import app.passwordstore.data.crypto.GPGPassphraseCache
import app.passwordstore.data.passfile.PasswordEntry
@ -41,7 +40,6 @@ import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
import logcat.logcat
@RequiresApi(Build.VERSION_CODES.O)
@AndroidEntryPoint
class AutofillDecryptActivity : BasePgpActivity() {

View file

@ -4,7 +4,6 @@
*/
package app.passwordstore.ui.autofill
import android.annotation.TargetApi
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@ -43,7 +42,6 @@ import kotlinx.coroutines.flow.onEach
import logcat.LogPriority.ERROR
import logcat.logcat
@TargetApi(Build.VERSION_CODES.O)
@AndroidEntryPoint
class AutofillFilterView : AppCompatActivity() {

View file

@ -4,7 +4,6 @@
*/
package app.passwordstore.ui.autofill
import android.annotation.TargetApi
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@ -33,7 +32,6 @@ import com.github.michaelbull.result.runCatching
import logcat.LogPriority.ERROR
import logcat.logcat
@TargetApi(Build.VERSION_CODES.O)
class AutofillPublisherChangedActivity : AppCompatActivity() {
companion object {

View file

@ -8,11 +8,9 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.view.autofill.AutofillManager
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import app.passwordstore.data.repo.PasswordRepository
@ -29,7 +27,6 @@ import java.io.File
import logcat.LogPriority.ERROR
import logcat.logcat
@RequiresApi(Build.VERSION_CODES.O)
@AndroidEntryPoint
class AutofillSaveActivity : AppCompatActivity() {

View file

@ -57,7 +57,7 @@ open class BasePgpActivity : AppCompatActivity() {
val name: String by unsafeLazy { File(fullPath).nameWithoutExtension }
/** Action to invoke if [keyImportAction] succeeds. */
var onKeyImport: (() -> Unit)? = null
private var onKeyImport: (() -> Unit)? = null
private val keyImportAction =
registerForActivityResult(StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
@ -94,10 +94,8 @@ open class BasePgpActivity : AppCompatActivity() {
) {
val clipboard = clipboard ?: return
val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
clip.description.extras =
PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) }
}
clip.description.extras =
PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) }
clipboard.setPrimaryClip(clip)
if (showSnackbar && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
snackbar(message = resources.getString(snackbarTextRes))
@ -144,11 +142,7 @@ open class BasePgpActivity : AppCompatActivity() {
action = ClipboardService.ACTION_START
putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(service)
} else {
startService(service)
}
startForegroundService(service)
}
}

View file

@ -8,9 +8,7 @@ package app.passwordstore.ui.settings
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatTextView
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
@ -35,11 +33,9 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
private val isAutofillServiceEnabled: Boolean
get() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
return activity.autofillManager?.hasEnabledAutofillServices() == true
}
@RequiresApi(Build.VERSION_CODES.O)
private fun showAutofillDialog(pref: SwitchPreference) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
@ -95,10 +91,8 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
builder.apply {
switch(PreferenceKeys.AUTOFILL_ENABLE) {
titleRes = R.string.pref_autofill_enable_title
visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
defaultValue = isAutofillServiceEnabled
onClick {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@onClick true
if (isAutofillServiceEnabled) {
activity.autofillManager?.disableAutofillServices()
} else {

View file

@ -6,7 +6,6 @@
package app.passwordstore.ui.settings
import android.content.pm.ShortcutManager
import android.os.Build
import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
@ -97,10 +96,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
activity.getSystemService<ShortcutManager>()?.apply {
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
}
activity.getSystemService<ShortcutManager>()?.apply {
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
}
false
}

View file

@ -8,7 +8,6 @@ package app.passwordstore.ui.settings
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentActivity
@ -47,11 +46,7 @@ class MiscSettings(activity: FragmentActivity) : SettingsProvider {
putExtra("uri", uri)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(service)
} else {
activity.startService(service)
}
activity.startForegroundService(service)
}
}

View file

@ -8,7 +8,6 @@ package app.passwordstore.ui.settings
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.core.content.edit
import androidx.core.content.getSystemService
@ -172,10 +171,8 @@ class RepositorySettings(
}
.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.getSystemService<ShortcutManager>()?.apply {
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
}
activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) }
dialogInterface.cancel()

View file

@ -5,8 +5,6 @@
package app.passwordstore.util.autofill
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.sharedPrefs
@ -101,7 +99,6 @@ enum class DirectoryStructure(val value: String) {
?: file.nameWithoutExtension
}
@RequiresApi(Build.VERSION_CODES.O)
fun getSaveFolderName(sanitizedIdentifier: String, username: String?) =
when (this) {
EncryptedUsername -> "/"

View file

@ -12,7 +12,6 @@ import android.service.autofill.Dataset
import android.service.autofill.FillCallback
import android.service.autofill.FillResponse
import android.service.autofill.SaveInfo
import androidx.annotation.RequiresApi
import app.passwordstore.autofill.oreo.ui.AutofillSmsActivity
import app.passwordstore.ui.autofill.AutofillDecryptActivity
import app.passwordstore.ui.autofill.AutofillFilterView
@ -32,7 +31,6 @@ import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
@RequiresApi(Build.VERSION_CODES.O)
class AutofillResponseBuilder
@AssistedInject
constructor(

View file

@ -21,7 +21,6 @@ import android.util.TypedValue
import android.view.View
import android.view.autofill.AutofillManager
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
@ -39,7 +38,7 @@ import logcat.logcat
/** Get an instance of [AutofillManager]. Only available on Android Oreo and above */
val Context.autofillManager: AutofillManager?
@RequiresApi(Build.VERSION_CODES.O) get() = getSystemService()
get() = getSystemService()
/** Get an instance of [ClipboardManager] */
val Context.clipboard

View file

@ -14,7 +14,6 @@ import android.content.ClipData
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import app.passwordstore.R
@ -40,11 +39,7 @@ class ClipboardService : Service() {
when (intent.action) {
ACTION_CLEAR -> {
clearClipboard()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION") stopForeground(true)
}
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
return super.onStartCommand(intent, flags, startId)
}
@ -60,11 +55,7 @@ class ClipboardService : Service() {
withContext(Dispatchers.IO) { startTimer(time) }
withContext(Dispatchers.Main) {
clearClipboard()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION") stopForeground(true)
}
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
@ -122,52 +113,23 @@ class ClipboardService : Service() {
val clearTimeMs = clearTime * 1000L
val clearIntent = Intent(this, ClipboardService::class.java).apply { action = ACTION_CLEAR }
val pendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(
this,
0,
clearIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
} else {
PendingIntent.getService(
this,
0,
clearIntent,
PendingIntent.FLAG_UPDATE_CURRENT,
)
}
val notification =
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
createNotificationApi23(pendingIntent)
} else {
createNotificationApi24(pendingIntent, clearTimeMs)
}
PendingIntent.getForegroundService(
this,
0,
clearIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val notification = createNotification(pendingIntent, clearTimeMs)
createNotificationChannel()
startForeground(1, notification)
}
private fun createNotificationApi23(pendingIntent: PendingIntent): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.tap_clear_clipboard))
.setSmallIcon(R.drawable.ic_action_secure_24dp)
.setContentIntent(pendingIntent)
.setUsesChronometer(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
@RequiresApi(Build.VERSION_CODES.N)
private fun createNotificationApi24(
pendingIntent: PendingIntent,
clearTimeMs: Long
): Notification {
private fun createNotification(pendingIntent: PendingIntent, clearTimeMs: Long): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.tap_clear_clipboard))
@ -182,19 +144,17 @@ class ClipboardService : Service() {
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>()
if (manager != null) {
manager.createNotificationChannel(serviceChannel)
} else {
logcat { "Failed to create notification channel" }
}
val serviceChannel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>()
if (manager != null) {
manager.createNotificationChannel(serviceChannel)
} else {
logcat { "Failed to create notification channel" }
}
}

View file

@ -13,7 +13,6 @@ import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import androidx.annotation.RequiresApi
import app.passwordstore.BuildConfig
import app.passwordstore.R
import app.passwordstore.ui.autofill.AutofillSaveActivity
@ -37,7 +36,6 @@ import javax.inject.Inject
import logcat.LogPriority.ERROR
import logcat.logcat
@RequiresApi(Build.VERSION_CODES.O)
@AndroidEntryPoint
class OreoAutofillService : AutofillService() {

View file

@ -10,7 +10,6 @@ import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.IntentCompat
@ -20,8 +19,6 @@ import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.TimeZone
import logcat.logcat
class PasswordExportService : Service() {
@ -65,13 +62,7 @@ class PasswordExportService : Service() {
logcat { "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 dateString = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val passDir = targetDirectory.createDirectory("password_store_$dateString")
if (passDir != null) {
@ -136,19 +127,17 @@ class PasswordExportService : Service() {
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>()
if (manager != null) {
manager.createNotificationChannel(serviceChannel)
} else {
logcat { "Failed to create notification channel" }
}
val serviceChannel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>()
if (manager != null) {
manager.createNotificationChannel(serviceChannel)
} else {
logcat { "Failed to create notification channel" }
}
}

View file

@ -10,8 +10,6 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import app.passwordstore.R
import app.passwordstore.data.password.PasswordItem
@ -42,7 +40,6 @@ constructor(
* [MAX_SHORTCUT_COUNT] and older items are removed by a simple LRU sweep.
*/
fun addDynamicShortcut(item: PasswordItem, intent: Intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return
val shortcutManager: ShortcutManager = context.getSystemService() ?: return
val shortcut = buildShortcut(item, intent)
val shortcuts = shortcutManager.dynamicShortcuts
@ -67,7 +64,6 @@ constructor(
* a no-op if the user's default launcher does not support pinned shortcuts.
*/
fun addPinnedShortcut(item: PasswordItem, intent: Intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val shortcutManager: ShortcutManager = context.getSystemService() ?: return
if (!shortcutManager.isRequestPinShortcutSupported) {
logcat { "addPinnedShortcut: pin shortcuts unsupported" }
@ -78,7 +74,6 @@ constructor(
}
/** Creates a [ShortcutInfo] from [item] and assigns [intent] to it. */
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun buildShortcut(item: PasswordItem, intent: Intent): ShortcutInfo {
return ShortcutInfo.Builder(context, item.fullPathToParent)
.setShortLabel(item.toString())
@ -93,7 +88,6 @@ constructor(
* data, which ensures that the get/set dance in [addDynamicShortcut] does not cause invalidation
* of icon assets, resulting in invisible icons in all but the newest launcher shortcut.
*/
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun rebuildShortcut(shortcut: ShortcutInfo): ShortcutInfo {
// Non-null assertions are fine since we know these values aren't null.
return ShortcutInfo.Builder(context, shortcut.id)

View file

@ -14,7 +14,6 @@ import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.view.autofill.AutofillManager
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import app.passwordstore.databinding.ActivityOreoAutofillSmsBinding
@ -52,7 +51,6 @@ suspend fun <T> Task<T>.suspendableAwait() =
}
}
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSmsActivity : AppCompatActivity() {
companion object {

View file

@ -2,6 +2,8 @@
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
@file:Suppress("UnstableApiUsage")
plugins {
id("com.github.android-password-store.published-android-library")
id("com.github.android-password-store.kotlin-android")
@ -9,7 +11,10 @@ plugins {
}
android {
defaultConfig { consumerProguardFiles("consumer-rules.pro") }
defaultConfig {
minSdk = 23
consumerProguardFiles("consumer-rules.pro")
}
sourceSets { getByName("test") { resources.srcDir("src/main/assets") } }
namespace = "com.github.androidpasswordstore.autofillparser"
}

View file

@ -19,14 +19,8 @@ object AndroidCommon {
project.extensions.configure<TestedExtension> {
setCompileSdkVersion(33)
defaultConfig {
minSdk = 23
targetSdk = 31
}
sourceSets {
named("main") { java.srcDirs("src/main/kotlin") }
named("test") { java.srcDirs("src/test/kotlin") }
named("androidTest") { java.srcDirs("src/androidTest/kotlin") }
minSdk = 26
targetSdk = 33
}
packagingOptions {
@ -47,7 +41,8 @@ object AndroidCommon {
animationsDisabled = true
unitTests.isReturnDefaultValues = true
}
project.tasks.withType<Test> {
project.tasks.withType<Test>().configureEach {
jvmArgs(
"--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED",

View file

@ -23,7 +23,9 @@ class PublishedAndroidLibraryPlugin : Plugin<Project> {
}
project.extensions.configure<MavenPublishBaseExtension> {
publishToMavenCentral(SonatypeHost.DEFAULT, true)
signAllPublications()
if (project.providers.environmentVariable("CI").isPresent) {
signAllPublications()
}
}
project.extensions.configure<MetalavaExtension> {
documentation.set(Documentation.PUBLIC)