diff --git a/CHANGELOG b/CHANGELOG index ad8ae1817..d1526d50b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +KeePassDX(2.5beta30) + * Fix Lock after screen off (wait 1.5 seconds) + * Upgrade autofill algorithm + * Fix ANR during file verifications + KeePassDX(2.5beta29) * Upgrade autofill algorithm * Delete registered KeyFile after save new credentials diff --git a/app/build.gradle b/app/build.gradle index d88804569..4ed1e26eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 29 - versionCode = 29 - versionName = "2.5beta29" + versionCode = 30 + versionName = "2.5beta30" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index c542464c0..1ea62dcf0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -509,7 +509,7 @@ class EntryEditActivity : LockingActivity(), AlertDialog.Builder(this) .setMessage(R.string.discard_changes) .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok) { _, _ -> + .setPositiveButton(R.string.discard) { _, _ -> super@EntryEditActivity.onBackPressed() }.create().show() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index e3aee34df..4d7d0dbf5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -35,6 +35,7 @@ import android.view.View import android.widget.TextView import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator @@ -61,6 +62,7 @@ class FileDatabaseSelectActivity : StylishActivity(), AssignMasterKeyDialogFragment.AssignPasswordDialogListener { // Views + private var coordinatorLayout: CoordinatorLayout? = null private var fileListContainer: View? = null private var createButtonView: View? = null private var openDatabaseButtonView: View? = null @@ -82,6 +84,7 @@ class FileDatabaseSelectActivity : StylishActivity(), mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) setContentView(R.layout.activity_file_selection) + coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout) fileListContainer = findViewById(R.id.container_file_list) val toolbar = findViewById(R.id.toolbar) @@ -179,7 +182,9 @@ class FileDatabaseSelectActivity : StylishActivity(), private fun fileNoFoundAction(e: FileNotFoundException) { val error = getString(R.string.file_not_found_content) - Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() + coordinatorLayout?.let { + Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show() + } Log.e(TAG, error, e) } @@ -278,9 +283,8 @@ class FileDatabaseSelectActivity : StylishActivity(), // Show only uri accessible historyList.filter { if (hideBrokenLocations) { - UriUtil.parse(it.databaseUri)?.let { historyUri -> - UriUtil.isUriAccessible(contentResolver, historyUri) - } ?: false + FileDatabaseInfo(this@FileDatabaseSelectActivity, + it.databaseUri).exists } else true }) @@ -369,10 +373,13 @@ class FileDatabaseSelectActivity : StylishActivity(), if (mDatabaseFileUri != null) { AssignMasterKeyDialogFragment.getInstance(true) .show(supportFragmentManager, "passwordDialog") + } else { + val error = getString(R.string.error_create_database) + coordinatorLayout?.let { + Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show() + } + Log.e(TAG, error) } - // else { - // TODO Show error - // } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index c19558ed0..b935f43eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -313,7 +313,13 @@ open class PasswordActivity : StylishActivity() { } private fun initUriFromIntent() { - mForceReadOnly = !UriUtil.isUriWritable(mDatabaseFileUri) + /* + // "canXrite" doesn't work with Google Drive, don't really know why? + mForceReadOnly = mDatabaseFileUri?.let { + !FileDatabaseInfo(this, it).canWrite + } ?: false + */ + mForceReadOnly = false // Post init uri with KeyFile if needed if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) { diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/FileDatabaseHistoryAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/FileDatabaseHistoryAdapter.kt index 3d01189b4..45a4313df 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/FileDatabaseHistoryAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/FileDatabaseHistoryAdapter.kt @@ -84,24 +84,25 @@ class FileDatabaseHistoryAdapter(private val context: Context) // File path holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString()) - if (fileDatabaseInfo.dataAccessible()) { + if (fileDatabaseInfo.exists) { holder.fileInformation.clearColorFilter() } else { holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY) } // Modification - if (fileDatabaseInfo.lastModificationAccessible()) { - holder.fileModification.text = fileDatabaseInfo.getModificationString() + fileDatabaseInfo.getModificationString()?.let { + holder.fileModification.text = it holder.fileModification.visibility = View.VISIBLE - } else { + } ?: run { holder.fileModification.visibility = View.GONE } + // Size - if (fileDatabaseInfo.sizeAccessible()) { - holder.fileSize.text = fileDatabaseInfo.getSizeString() + fileDatabaseInfo.getSizeString()?.let { + holder.fileSize.text = it holder.fileSize.visibility = View.VISIBLE - } else { + } ?: run { holder.fileSize.visibility = View.GONE } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index 6c01b0d87..c22fe810a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -34,7 +34,6 @@ import java.util.* internal class StructureParser(private val structure: AssistStructure) { private var result: Result? = null private var usernameCandidate: AutofillId? = null - private var lockHint: Boolean = false fun parse(): Result? { result = Result() @@ -98,15 +97,12 @@ internal class StructureParser(private val structure: AssistStructure) { Log.d(TAG, "Autofill password hint") return true } - it.toLowerCase(Locale.ENGLISH) == "off" -> { - Log.d(TAG, "Autofill OFF hint") - lockHint = true - return false - } + // Ignore autocomplete="off" + // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion + it.toLowerCase(Locale.ENGLISH) == "off" || it.toLowerCase(Locale.ENGLISH) == "on" -> { - Log.d(TAG, "Autofill ON hint") - if (parseNodeByHtmlAttributes(node)) - return true + Log.d(TAG, "Autofill web hint") + return parseNodeByHtmlAttributes(node) } else -> Log.d(TAG, "Autofill unsupported hint $it") } @@ -115,8 +111,6 @@ internal class StructureParser(private val structure: AssistStructure) { } private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean { - if (lockHint) - return false val autofillId = node.autofillId val nodHtml = node.htmlInfo when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) { @@ -136,6 +130,7 @@ internal class StructureParser(private val structure: AssistStructure) { "password" -> { result?.passwordId = autofillId Log.d(TAG, "Autofill password type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}") + return true } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt index 7e889f86d..31b7709d5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt @@ -19,12 +19,15 @@ */ package com.kunzisoft.keepass.utils +import android.app.AlarmManager import android.app.NotificationManager +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context +import android.content.Context.ALARM_SERVICE import android.content.Intent import android.content.IntentFilter -import android.os.Handler +import android.os.Build import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database @@ -42,26 +45,34 @@ const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() { - private val screenOffHandler = Handler() - private var screenOffRunnable: Runnable? = null + var mLockPendingIntent: PendingIntent? = null override fun onReceive(context: Context, intent: Intent) { - - screenOffRunnable?.let { runnable -> - screenOffHandler.removeCallbacks(runnable) - } // If allowed, lock and exit if (!TimeoutHelper.temporarilyDisableTimeout) { intent.action?.let { when (it) { + Intent.ACTION_SCREEN_ON -> { + cancelLockPendingIntent(context) + } Intent.ACTION_SCREEN_OFF -> { if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) { - screenOffRunnable = Runnable { - lockAction.invoke() - } + mLockPendingIntent = PendingIntent.getBroadcast(context, + 4575, + Intent(intent).apply { + action = LOCK_ACTION + }, + 0) // Launch the effective action after a small time - screenOffHandler.postDelayed(screenOffRunnable!!, - context.getString(R.string.timeout_screen_off).toLong()) + val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong() + val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent) + } else { + alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent) + } + } else { + cancelLockPendingIntent(context) } } LOCK_ACTION, @@ -71,6 +82,14 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() { } } } + + private fun cancelLockPendingIntent(context: Context) { + mLockPendingIntent?.let { + val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager? + alarmManager?.cancel(mLockPendingIntent) + mLockPendingIntent = null + } + } } fun Context.registerLockReceiver(lockReceiver: LockReceiver?, diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/FileDatabaseInfo.kt b/app/src/main/java/com/kunzisoft/keepass/utils/FileDatabaseInfo.kt index e5cb50942..64acb532d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/FileDatabaseInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/FileDatabaseInfo.kt @@ -21,25 +21,77 @@ package com.kunzisoft.keepass.utils import android.content.Context import android.net.Uri +import android.text.format.Formatter +import androidx.documentfile.provider.DocumentFile import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.settings.PreferencesUtil +import java.io.Serializable +import java.text.DateFormat +import java.util.* -class FileDatabaseInfo : FileInfo { +class FileDatabaseInfo : Serializable { - constructor(context: Context, fileUri: Uri): super(context, fileUri) + private var context: Context + private var documentFile: DocumentFile? = null + var fileUri: Uri? + private set - constructor(context: Context, filePath: String): super(context, filePath) + constructor(context: Context, fileUri: Uri) { + this.context = context + this.fileUri = fileUri + init() + } + + constructor(context: Context, filePath: String) { + this.context = context + this.fileUri = UriUtil.parse(filePath) + init() + } + + fun init() { + documentFile = UriUtil.getFileData(context, fileUri) + } + + var exists: Boolean = false + get() { + return documentFile?.exists() ?: field + } + private set + + var canRead: Boolean = false + get() { + return documentFile?.canRead() ?: field + } + private set + + var canWrite: Boolean = false + get() { + return documentFile?.canWrite() ?: field + } + private set + + fun getModificationString(): String? { + return documentFile?.lastModified()?.let { + DateFormat.getDateTimeInstance() + .format(Date(it)) + } + } + + fun getSizeString(): String? { + return documentFile?.let { + Formatter.formatFileSize(context, it.length()) + } + } fun retrieveDatabaseAlias(alias: String): String { return when { alias.isNotEmpty() -> alias - PreferencesUtil.isFullFilePathEnable(context) -> filePath ?: "" - else -> fileName ?: "" + PreferencesUtil.isFullFilePathEnable(context) -> fileUri?.path ?: "" + else -> if (exists) documentFile?.name ?: "" else fileUri?.path ?: "" } } fun retrieveDatabaseTitle(titleCallback: (String)->Unit) { - fileUri?.let { fileUri -> FileDatabaseHistoryAction.getInstance(context.applicationContext) .getFileDatabaseHistory(fileUri) { fileDatabaseHistoryEntity -> @@ -48,5 +100,4 @@ class FileDatabaseInfo : FileInfo { } } } - } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/FileInfo.kt b/app/src/main/java/com/kunzisoft/keepass/utils/FileInfo.kt deleted file mode 100644 index d7a4a2b72..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/utils/FileInfo.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePassDX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePassDX. If not, see . - * - */ -package com.kunzisoft.keepass.utils - -import android.content.Context -import android.net.Uri -import android.text.format.Formatter -import java.io.Serializable -import java.text.DateFormat -import java.util.* - -open class FileInfo : Serializable { - - var context: Context - var fileUri: Uri? - var filePath: String? = null - var fileName: String? = "" - var lastModification = Date(0L) - var size: Long = 0L - - constructor(context: Context, fileUri: Uri) { - this.context = context - this.fileUri = fileUri - init() - } - - constructor(context: Context, filePath: String) { - this.context = context - this.fileUri = UriUtil.parse(filePath) - init() - } - - fun init() { - this.filePath = fileUri?.path - - UriUtil.getFileData(context, fileUri)?.let { file -> - size = file.length() - fileName = file.name - lastModification = Date(file.lastModified()) - } - - if (fileName == null || fileName!!.isEmpty()) { - fileName = filePath - } - } - - fun lastModificationAccessible(): Boolean { - return lastModification.after(Date(0L)) - } - - fun sizeAccessible(): Boolean { - return size != 0L - } - - fun dataAccessible(): Boolean { - return UriUtil.isUriAccessible(context.contentResolver, fileUri) - } - - fun getModificationString(): String { - return DateFormat.getDateTimeInstance() - .format(lastModification) - } - - fun getSizeString(): String { - return Formatter.formatFileSize(context, size) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt index 4872055d1..14b17346c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -24,7 +24,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build -import android.util.Log import android.widget.Toast import androidx.documentfile.provider.DocumentFile import com.kunzisoft.keepass.R @@ -37,26 +36,6 @@ import java.util.* object UriUtil { - fun isUriAccessible(contentResolver: ContentResolver, fileUri: Uri?): Boolean { - if (fileUri == null) - return false - return try { - //https://developer.android.com/reference/android/content/res/AssetFileDescriptor - contentResolver.openInputStream(fileUri)?.close() - true - } catch (e: Exception) { - Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}") - false - } - } - - fun isUriWritable(fileUri: Uri?): Boolean { - if (fileUri == null) - return false - // TODO Uri writeable detection - return true - } - fun getFileData(context: Context, fileUri: Uri?): DocumentFile? { if (fileUri == null) return null diff --git a/app/src/main/res/layout/view_entry_contents.xml b/app/src/main/res/layout/view_entry_contents.xml index 496cf079e..73c73ee0f 100644 --- a/app/src/main/res/layout/view_entry_contents.xml +++ b/app/src/main/res/layout/view_entry_contents.xml @@ -424,6 +424,16 @@ android:layout_height="wrap_content" android:text="@string/entry_UUID" style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" /> + + + @@ -434,21 +444,6 @@ android:textIsSelectable="true" style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" /> - - - diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 0f44c87e2..9fb0a4825 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -245,7 +245,7 @@ timeout_backup_key 300000 - 3000 + 1500 5000 10000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72c716e47..3367183dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,7 @@ Entry icon Validate Discard changes? + Discard Password generator Password length Add field @@ -123,6 +124,7 @@ You can not move an entry here. You can not copy an entry here. You can not copy a group here. + Unable to create database file. Unable to create database with this password and keyfile. Could not save database. Secret key must be in Base32 format. @@ -207,7 +209,7 @@ Write-protected Depending on your file manager, KeePassDX may not allowed to write in your storage. The database contains duplicate UUIDs. - By validating this dialog, KeePassDX will fix the problem (by generating new UUIDs for duplicates) and continue. + Solve problem by generating new UUIDs for duplicates to continue? Selection mode Save location of databases Remember the location of databases @@ -252,9 +254,9 @@ Avoid password characters outside of text encoding format in database file (unrecognized chars are converted to the same letter). Grant file write access to save database changes Mount the memory card to create or load a database. - Do you really want no password unlocking protection? - Are you sure you do not want to use any encryption key? - Are you sure you want to permanently delete the selected nodes? + Continue without password unlocking protection? + Continue without encryption key? + Permanently delete selected nodes? Version %1$s Build %1$s Biometric prompt is supported but not set up. @@ -300,11 +302,11 @@ Use advanced unlocking to open a database more easily Biometric unlocking Lets you scan your biometric to open the database - Auto open biometric prompt + Auto-open biometric prompt Automatically open biometric prompt when a biometric key is defined for a database Delete encryption keys Delete all encryption keys related to biometric recognition - Are you sure you want to delete all keys related to biometric recognition? + Delete all encryption keys related to biometric recognition? Could not start this feature. Your Android version %1$s does not meet the minimum version %2$s required. Could not find the corresponding hardware. @@ -326,7 +328,7 @@ Recommend changing the master key (days) Force renewal Require changing the master key (days) - Force renewal the next time + Force renewal next time Require changing the master key the next time (once) Field font Change font used in fields for better character visibility @@ -352,7 +354,7 @@ Keyboard Magikeyboard Activate a custom keyboard populating your passwords and all identity fields - Device keyboard Settings + Device keyboard settings Magikeyboard Magikeyboard (KeePassDX) Magikeyboard settings @@ -372,7 +374,7 @@ Keyboard theme Keys Auto key action - Action of the "Go" key performed automatically after pressing a "Field" key + \"Go\" key action after pressing a \"Field\" key Vibratory keypresses Audible keypresses Allow no master key @@ -382,7 +384,7 @@ Write-protected Open the database read-only by default Autosave database - Automatically save the database after an important action (only in \"Modifiable\" mode) + Save the database after every important action (in \"Modifiable\" mode) Educational screens Highlight the elements to learn how the app works Reset educational screens @@ -474,5 +476,5 @@ Icon pack Icon pack used in the app Hide expired entries - Expired entries will be hidden + Expired entries are hidden \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/30.txt b/fastlane/metadata/android/en-US/changelogs/30.txt new file mode 100644 index 000000000..6cc4e68f0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/30.txt @@ -0,0 +1,3 @@ + * Fix Lock after screen off (wait 1.5 seconds) + * Upgrade autofill algorithm + * Fix ANR during file verification diff --git a/fastlane/metadata/android/fr-FR/changelogs/30.txt b/fastlane/metadata/android/fr-FR/changelogs/30.txt new file mode 100644 index 000000000..e440e0489 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/30.txt @@ -0,0 +1,3 @@ + * Correction du verrouillage après la désactivation de l'écran (attendre 1,5 seconde) + * Mise à niveau de l'algorithme de remplissage automatique + * Correction de l'ANR lors de la vérification de fichier \ No newline at end of file