diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3475642d2..799b9a4f4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen. - Created with: [e.g Windows KeePass 2.42] - Version: [e.g. 2] - Location: [e.g. Remote file retrieved with GDrive app] + - File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`] - Size: [e.g. 150Mo] - Contains attachment: [e.g. Yes] diff --git a/CHANGELOG b/CHANGELOG index eb0e1374c..d2bcddba4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,33 @@ +KeePassDX(3.2.0) + * Manage data merge #840 #977 + * Manage Tags #633 + * Inherit colors and icon from template #1213 #1130 + * Setting to keep the screen on when watching the entry #1119 + * Add path in quick search + * Small fixes + KeePassDX(3.1.0) + * Add breadcrumb + * Add path in search results #1148 + * Add group info dialog #1177 + * Manage colors #64 #913 + * Fix UI in Android 8 #509 + * Upgrade libs and SDK to 31 #833 + * Fix parser of database v1 #1201 + * Stop asking WRITE_EXTERNAL_STORAGE permission + +KeePassDX(3.0.4) + * Fix autofill inline bugs #1173 #1165 + * Small UI change + +KeePassDX(3.0.3) * Change default Argon2 parameters #1098 * Add & edit custom icon name #976 - * Manage Tags #633 + * Fix templates #1128 #1133 #1138 + * Update Autofill compatibility list #725 #1154 + * Improve fingerprint usage #1137 #1145 + * Change backup configuration #1144 + * Add lock button in database notification KeePassDX(3.0.2) * Samsung DeX mode #1114 #245 (Thx @chenxiaolong) diff --git a/README.md b/README.md index 7fee13a6d..b8389c0a8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - Material design with **themes**. - **Auto-Fill** and Integration. - Field filling **keyboard**. + - Dynamic **templates** - **History** of each entry. - Precise management of **settings**. - Code written in **native languages** *(Kotlin / Java / JNI / C)*. @@ -71,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w ## License - Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). + Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). This file is part of KeePassDX. diff --git a/app/build.gradle b/app/build.gradle index f89f6d474..fbb6160d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,16 +3,16 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdkVersion 31 + buildToolsVersion "31.0.0" ndkVersion "21.4.7075529" defaultConfig { applicationId "com.kunzisoft.keepass" minSdkVersion 15 - targetSdkVersion 30 - versionCode = 90 - versionName = "3.1.0" + targetSdkVersion 31 + versionCode = 93 + versionName = "3.2.0" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" @@ -99,22 +99,22 @@ android { } } -def room_version = "2.2.6" +def room_version = "2.4.1" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.biometric:biometric:1.1.0' + implementation 'androidx.media:media:1.4.3' // Lifecycle - LiveData - ViewModel - Coroutines - implementation "androidx.core:core-ktx:1.3.2" - implementation 'androidx.fragment:fragment-ktx:1.2.5' - // WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923 - implementation 'com.google.android.material:material:1.1.0' + implementation "androidx.core:core-ktx:$android_core_version" + implementation 'androidx.fragment:fragment-ktx:1.4.0' + implementation "com.google.android.material:material:$android_material_version" // Token auto complete implementation "com.splitwise:tokenautocomplete:4.0.0-beta04" // Database @@ -123,11 +123,11 @@ dependencies { // Autofill implementation "androidx.autofill:autofill:1.1.0" // Time - implementation 'joda-time:joda-time:2.10.6' + implementation 'joda-time:joda-time:2.10.13' // Color - implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' + implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6' // Education - implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' + implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' // Apache Commons implementation 'commons-io:commons-io:2.8.0' implementation 'commons-codec:commons-codec:1.15' @@ -138,6 +138,6 @@ dependencies { implementation project(path: ':icon-pack-material') // Tests - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation "androidx.test:runner:$android_test_version" + androidTestImplementation "androidx.test:rules:$android_test_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e9f9f85d..fef19294a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,15 +10,12 @@ android:anyDensity="true" /> + - - + tools:targetApi="s"> @@ -44,6 +42,7 @@ android:theme="@style/KeepassDXStyle.SplashScreen" android:label="@string/app_name" android:launchMode="singleTop" + android:exported="true" android:configChanges="keyboardHidden" android:windowSoftInputMode="stateHidden|stateAlwaysHidden" > @@ -53,6 +52,7 @@ @@ -111,6 +111,7 @@ + android:theme="@style/Theme.Transparent" + android:exported="true"> @@ -173,7 +175,8 @@ android:theme="@style/Theme.Transparent" /> + android:label="@string/keyboard_setting_label" + android:exported="true"> @@ -199,6 +202,7 @@ diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt index 43a73b1fb..44e0dda98 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt @@ -23,28 +23,33 @@ import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.IntentSender import android.os.Build -import android.view.inputmethod.InlineSuggestionsRequest +import android.os.Bundle import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST +import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.utils.LOCK_ACTION @RequiresApi(api = Build.VERSION_CODES.O) class AutofillLauncherActivity : DatabaseModeActivity() { + private var mAutofillActivityResultLauncher: ActivityResultLauncher? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + AutofillHelper.buildActivityResultLauncher(this, true) + else null + override fun applyCustomStyle(): Boolean { return false } @@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() { EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> when (specialMode) { SpecialMode.SELECTION -> { - // Build search param - val searchInfo = SearchInfo().apply { - applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) - webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) - webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) - manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false) - } - SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> - searchInfo.webDomain = concreteWebDomain - launchSelection(database, searchInfo) + intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle -> + // To pass extra inline request + var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION) + } + // Build search param + bundle.getParcelable(KEY_SEARCH_INFO)?.let { searchInfo -> + SearchInfo.getConcreteWebDomain( + this, + searchInfo.webDomain + ) { concreteWebDomain -> + // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) + val assistStructure = AutofillHelper + .retrieveAutofillComponent(intent) + ?.assistStructure + val newAutofillComponent = if (assistStructure != null) { + AutofillComponent( + assistStructure, + compatInlineSuggestionsRequest + ) + } else { + null + } + searchInfo.webDomain = concreteWebDomain + launchSelection(database, newAutofillComponent, searchInfo) + } + } } + // Remove bundle + intent.removeExtra(KEY_SELECTION_BUNDLE) } SpecialMode.REGISTRATION -> { // To register info @@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() { } private fun launchSelection(database: Database?, + autofillComponent: AutofillComponent?, searchInfo: SearchInfo) { - // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) - val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent) - if (autofillComponent == null) { setResult(Activity.RESULT_CANCELED) finish() @@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() { // Show the database UI to select the entry GroupActivity.launchForAutofillResult(this, openedDatabase, + mAutofillActivityResultLauncher, autofillComponent, searchInfo, false) @@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() { { // If database not open FileDatabaseSelectActivity.launchForAutofillResult(this, + mAutofillActivityResultLauncher, autofillComponent, searchInfo) } @@ -186,55 +211,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() { Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) - - if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) { - // Close the database - sendBroadcast(Intent(LOCK_ACTION)) - } - - super.onActivityResult(requestCode, resultCode, data) - } - companion object { - private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION" - private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID" - private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" - private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME" + private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" + private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" + private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" fun getPendingIntentForSelection(context: Context, searchInfo: SearchInfo? = null, - inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent { + compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent { return PendingIntent.getActivity(context, 0, - // Doesn't work with Parcelable (don't know why?) - Intent(context, AutofillLauncherActivity::class.java).apply { - searchInfo?.let { - putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) - putExtra(KEY_SEARCH_DOMAIN, it.webDomain) - putExtra(KEY_SEARCH_SCHEME, it.webScheme) - putExtra(KEY_MANUAL_SELECTION, it.manualSelection) - } + // Doesn't work with direct extra Parcelable (don't know why?) + // Wrap into a bundle to bypass the problem + Intent(context, AutofillLauncherActivity::class.java).apply { + putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { + putParcelable(KEY_SEARCH_INFO, searchInfo) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - inlineSuggestionsRequest?.let { - putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) - } + putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest) } - }, - PendingIntent.FLAG_CANCEL_CURRENT) + }) + }, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + } else { + PendingIntent.FLAG_CANCEL_CURRENT + }) } fun getPendingIntentForRegistration(context: Context, registerInfo: RegisterInfo): PendingIntent { return PendingIntent.getActivity(context, 0, - Intent(context, AutofillLauncherActivity::class.java).apply { - EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) - putExtra(KEY_REGISTER_INFO, registerInfo) - }, - PendingIntent.FLAG_CANCEL_CURRENT) + Intent(context, AutofillLauncherActivity::class.java).apply { + EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) + putExtra(KEY_REGISTER_INFO, registerInfo) + }, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + } else { + PendingIntent.FLAG_CANCEL_CURRENT + }) } fun launchForRegistration(context: Context, diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 114c3ba16..f461db19a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -32,12 +32,18 @@ import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.ProgressBar +import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import androidx.core.graphics.ColorUtils +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.CollapsingToolbarLayout +import com.google.android.material.progressindicator.LinearProgressIndicator import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.fragments.EntryFragment import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper @@ -60,22 +66,26 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.* +import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.utils.UuidUtil +import com.kunzisoft.keepass.view.changeControlColor +import com.kunzisoft.keepass.view.changeTitleColor import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.EntryViewModel import java.util.* -import kotlin.collections.HashMap class EntryActivity : DatabaseLockActivity() { private var coordinatorLayout: CoordinatorLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null + private var appBarLayout: AppBarLayout? = null private var titleIconView: ImageView? = null private var historyView: View? = null private var tagsListView: RecyclerView? = null private var tagsAdapter: TagsAdapter? = null - private var entryProgress: ProgressBar? = null + private var entryProgress: LinearProgressIndicator? = null private var lockView: View? = null private var toolbar: Toolbar? = null private var loadingView: ProgressBar? = null @@ -89,11 +99,21 @@ class EntryActivity : DatabaseLockActivity() { private var mEntryLoaded = false private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null - private var mAttachmentsToDownload: HashMap = HashMap() private var mExternalFileHelper: ExternalFileHelper? = null + private var mAttachmentSelected: Attachment? = null + + private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { + // Reload the current id from database + mEntryViewModel.loadDatabase(mDatabase) + } private var mIcon: IconImage? = null - private var mIconColor: Int = 0 + private var mColorAccent: Int = 0 + private var mControlColor: Int = 0 + private var mColorPrimary: Int = 0 + private var mColorBackground: Int = 0 + private var mBackgroundColor: Int? = null + private var mForegroundColor: Int? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -108,6 +128,7 @@ class EntryActivity : DatabaseLockActivity() { // Get views coordinatorLayout = findViewById(R.id.toolbar_coordinator) collapsingToolbarLayout = findViewById(R.id.toolbar_layout) + appBarLayout = findViewById(R.id.app_bar) titleIconView = findViewById(R.id.entry_icon) historyView = findViewById(R.id.history_container) tagsListView = findViewById(R.id.entry_tags_list_view) @@ -119,10 +140,19 @@ class EntryActivity : DatabaseLockActivity() { collapsingToolbarLayout?.title = " " toolbar?.title = " " - // Retrieve the textColor to tint the icon - val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) - mIconColor = taIconColor.getColor(0, Color.BLACK) - taIconColor.recycle() + // Retrieve the textColor to tint the toolbar + val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) + val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl)) + val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)) + val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground)) + mColorAccent = taColorAccent.getColor(0, Color.BLACK) + mControlColor = taControlColor.getColor(0, Color.BLACK) + mColorPrimary = taColorPrimary.getColor(0, Color.BLACK) + mColorBackground = taColorBackground.getColor(0, Color.BLACK) + taColorAccent.recycle() + taControlColor.recycle() + taColorPrimary.recycle() + taColorBackground.recycle() // Init Tags adapter tagsAdapter = TagsAdapter(this) @@ -146,6 +176,15 @@ class EntryActivity : DatabaseLockActivity() { // Init SAF manager mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildCreateDocument { createdFileUri -> + mAttachmentSelected?.let { attachment -> + if (createdFileUri != null) { + mAttachmentFileBinderManager + ?.startDownloadAttachment(createdFileUri, attachment) + } + mAttachmentSelected = null + } + } // Init attachment service binder manager mAttachmentFileBinderManager = AttachmentFileBinderManager(this) @@ -165,10 +204,8 @@ class EntryActivity : DatabaseLockActivity() { // Assign history dedicated view historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE if (entryIsHistory) { - val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) collapsingToolbarLayout?.contentScrim = - ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) - taColorAccent.recycle() + ColorDrawable(mColorAccent) } val entryInfo = entryInfoHistory.entryInfo @@ -183,12 +220,9 @@ class EntryActivity : DatabaseLockActivity() { } // Assign title icon mIcon = entryInfo.icon - titleIconView?.let { iconView -> - mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor) - } // Assign title text val entryTitle = - if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString() + if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id) collapsingToolbarLayout?.title = entryTitle toolbar?.title = entryTitle mUrl = entryInfo.url @@ -196,6 +230,9 @@ class EntryActivity : DatabaseLockActivity() { val tags = entryInfo.tags tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE tagsAdapter?.setTags(tags) + // Assign colors + mBackgroundColor = entryInfo.backgroundColor + mForegroundColor = entryInfo.foregroundColor loadingView?.hideByFading() mEntryLoaded = true @@ -207,9 +244,9 @@ class EntryActivity : DatabaseLockActivity() { } mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement -> - if (otpElement == null) + if (otpElement == null) { entryProgress?.visibility = View.GONE - when (otpElement?.type) { + } else when (otpElement.type) { // Only add token if HOTP OtpType.HOTP -> { entryProgress?.visibility = View.GONE @@ -218,7 +255,7 @@ class EntryActivity : DatabaseLockActivity() { OtpType.TOTP -> { entryProgress?.apply { max = otpElement.period - progress = otpElement.secondsRemaining + setProgressCompat(otpElement.secondsRemaining, true) visibility = View.VISIBLE } } @@ -226,9 +263,8 @@ class EntryActivity : DatabaseLockActivity() { } mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected -> - mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode -> - mAttachmentsToDownload[requestCode] = attachmentSelected - } + mAttachmentSelected = attachmentSelected + mExternalFileHelper?.createDocument(attachmentSelected.name) } mEntryViewModel.historySelected.observe(this) { historySelected -> @@ -237,7 +273,8 @@ class EntryActivity : DatabaseLockActivity() { this, database, historySelected.nodeId, - historySelected.historyPosition + historySelected.historyPosition, + mEntryActivityResultLauncher ) } } @@ -255,13 +292,6 @@ class EntryActivity : DatabaseLockActivity() { super.onDatabaseRetrieved(database) mEntryViewModel.loadDatabase(database) - - // Assign title icon - mIcon?.let { icon -> - titleIconView?.let { iconView -> - mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor) - } - } } override fun onDatabaseActionFinished( @@ -299,6 +329,11 @@ class EntryActivity : DatabaseLockActivity() { } } } + + // Keep the screen on + if (PreferencesUtil.isKeepScreenOnEnabled(this)) { + window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } } override fun onPause() { @@ -307,24 +342,27 @@ class EntryActivity : DatabaseLockActivity() { super.onPause() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - when (requestCode) { - EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { - // Reload the current id from database - mEntryViewModel.loadDatabase(mDatabase) - } - } - - mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri -> - if (createdFileUri != null) { - mAttachmentsToDownload[requestCode]?.let { attachmentToDownload -> - mAttachmentFileBinderManager - ?.startDownloadAttachment(createdFileUri, attachmentToDownload) - } + private fun applyToolbarColors() { + appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary) + collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary) + val backgroundDarker = if (mBackgroundColor != null) { + ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f) + } else { + mColorBackground + } + titleIconView?.background?.colorFilter = BlendModeColorFilterCompat + .createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN) + mIcon?.let { icon -> + titleIconView?.let { iconView -> + mIconDrawableFactory?.assignDatabaseIcon( + iconView, + icon, + mForegroundColor ?: mColorAccent + ) } } + toolbar?.changeControlColor(mForegroundColor ?: mControlColor) + collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -358,11 +396,17 @@ class EntryActivity : DatabaseLockActivity() { } if (mEntryIsHistory || mDatabaseReadOnly) { menu?.findItem(R.id.menu_save_database)?.isVisible = false + menu?.findItem(R.id.menu_merge_database)?.isVisible = false menu?.findItem(R.id.menu_edit)?.isVisible = false } + if (!mMergeDataAllowed) { + menu?.findItem(R.id.menu_merge_database)?.isVisible = false + } if (mSpecialMode != SpecialMode.DEFAULT) { + menu?.findItem(R.id.menu_merge_database)?.isVisible = false menu?.findItem(R.id.menu_reload_database)?.isVisible = false } + applyToolbarColors() return super.onPrepareOptionsMenu(menu) } @@ -408,7 +452,8 @@ class EntryActivity : DatabaseLockActivity() { EntryEditActivity.launchToUpdate( this, database, - entryId + entryId, + mEntryActivityResultLauncher ) } } @@ -437,6 +482,9 @@ class EntryActivity : DatabaseLockActivity() { R.id.menu_save_database -> { saveDatabase() } + R.id.menu_merge_database -> { + mergeDatabase() + } R.id.menu_reload_database -> { reloadDatabase() } @@ -449,7 +497,7 @@ class EntryActivity : DatabaseLockActivity() { // Transit data in previous Activity after an update Intent().apply { putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) - setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this) + setResult(Activity.RESULT_OK, this) } super.finish() } @@ -467,15 +515,13 @@ class EntryActivity : DatabaseLockActivity() { */ fun launch(activity: Activity, database: Database, - entryId: NodeId) { + entryId: NodeId, + activityResultLauncher: ActivityResultLauncher) { if (database.loaded) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { val intent = Intent(activity, EntryActivity::class.java) intent.putExtra(KEY_ENTRY, entryId) - activity.startActivityForResult( - intent, - EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE - ) + activityResultLauncher.launch(intent) } } } @@ -486,16 +532,14 @@ class EntryActivity : DatabaseLockActivity() { fun launch(activity: Activity, database: Database, entryId: NodeId, - historyPosition: Int) { + historyPosition: Int, + activityResultLauncher: ActivityResultLauncher) { if (database.loaded) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { val intent = Intent(activity, EntryActivity::class.java) intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) - activity.startActivityForResult( - intent, - EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE - ) + activityResultLauncher.launch(intent) } } } 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 adff0943d..3afc45150 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -33,12 +33,17 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.* +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* @@ -69,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.* +import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import org.joda.time.DateTime import java.util.* @@ -96,6 +102,9 @@ class EntryEditActivity : DatabaseLockActivity(), private var mTemplate: Template? = null private var mIsTemplate: Boolean = false private var mEntryLoaded: Boolean = false + private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null + + private val mColorPickerViewModel: ColorPickerViewModel by viewModels() private var mAllowCustomFields = false private var mAllowOTP = false @@ -106,6 +115,10 @@ class EntryEditActivity : DatabaseLockActivity(), // Education private var entryEditActivityEducation: EntryEditActivityEducation? = null + private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon -> + mEntryEditViewModel.selectIcon(icon) + } + // To ask data lost only one time private var backPressedAlreadyApproved = false @@ -154,6 +167,21 @@ class EntryEditActivity : DatabaseLockActivity(), // To retrieve attachment mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildOpenDocument { uri -> + uri?.let { attachmentToUploadUri -> + UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile -> + documentFile.name?.let { fileName -> + if (documentFile.length() > MAX_WARNING_BINARY_FILE) { + FileTooBigDialogFragment.build(attachmentToUploadUri, fileName) + .show(supportFragmentManager, "fileTooBigFragment") + } else { + mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName) + } + } + } + } + } + mAttachmentFileBinderManager = AttachmentFileBinderManager(this) // Verify the education views entryEditActivityEducation = EntryEditActivityEducation(this) @@ -175,11 +203,13 @@ class EntryEditActivity : DatabaseLockActivity(), templateSelectorSpinner?.apply { // Build template selector if (templates.isNotEmpty()) { - adapter = TemplatesSelectorAdapter( + mTemplatesSelectorAdapter = TemplatesSelectorAdapter( this@EntryEditActivity, - mIconDrawableFactory, templates - ) + ).apply { + iconDrawableFactory = mIconDrawableFactory + } + adapter = mTemplatesSelectorAdapter val selectedTemplate = if (mTemplate != null) mTemplate else @@ -213,7 +243,16 @@ class EntryEditActivity : DatabaseLockActivity(), // View model listeners mEntryEditViewModel.requestIconSelection.observe(this) { iconImage -> - IconPickerActivity.launch(this@EntryEditActivity, iconImage) + IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher) + } + + mEntryEditViewModel.requestColorSelection.observe(this) { color -> + ColorPickerDialogFragment.newInstance(color) + .show(supportFragmentManager, "ColorPickerFragment") + } + + mColorPickerViewModel.colorPicked.observe(this) { color -> + mEntryEditViewModel.selectColor(color) } mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> @@ -321,6 +360,10 @@ class EntryEditActivity : DatabaseLockActivity(), mAllowCustomFields = database?.allowEntryCustomFields() == true mAllowOTP = database?.allowOTP == true mEntryEditViewModel.loadDatabase(database) + mTemplatesSelectorAdapter?.apply { + iconDrawableFactory = mIconDrawableFactory + notifyDataSetChanged() + } } override fun onDatabaseActionFinished( @@ -472,29 +515,6 @@ class EntryEditActivity : DatabaseLockActivity(), } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> - mEntryEditViewModel.selectIcon(icon) - } - - mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri -> - uri?.let { attachmentToUploadUri -> - UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile -> - documentFile.name?.let { fileName -> - if (documentFile.length() > MAX_WARNING_BINARY_FILE) { - FileTooBigDialogFragment.build(attachmentToUploadUri, fileName) - .show(supportFragmentManager, "fileTooBigFragment") - } else { - mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName) - } - } - } - } - } - } - /** * Set up OTP (HOTP or TOTP) and add it as extra field */ @@ -585,7 +605,7 @@ class EntryEditActivity : DatabaseLockActivity(), && entryEditActivityEducation.checkAndPerformedAttachmentEducation( attachmentView, { - mExternalFileHelper?.openDocument() + addNewAttachment() }, { performedNextEducation(entryEditActivityEducation) @@ -686,7 +706,7 @@ class EntryEditActivity : DatabaseLockActivity(), val intentEntry = Intent() bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId) intentEntry.putExtras(bundle) - setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry) + setResult(Activity.RESULT_OK, intentEntry) super.finish() } catch (e: Exception) { // Exception when parcelable can't be done @@ -701,23 +721,46 @@ class EntryEditActivity : DatabaseLockActivity(), // Keys for current Activity const val KEY_ENTRY = "entry" const val KEY_PARENT = "parent" - - // Keys for callback - const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31 - const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129 const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY" + fun registerForEntryResult(fragment: Fragment, + entryAddedOrUpdatedListener: (NodeId?) -> Unit): ActivityResultLauncher { + return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + entryAddedOrUpdatedListener.invoke( + result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY) + ) + } else { + entryAddedOrUpdatedListener.invoke(null) + } + } + } + + fun registerForEntryResult(activity: FragmentActivity, + entryAddedOrUpdatedListener: (NodeId?) -> Unit): ActivityResultLauncher { + return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + entryAddedOrUpdatedListener.invoke( + result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY) + ) + } else { + entryAddedOrUpdatedListener.invoke(null) + } + } + } + /** * Launch EntryEditActivity to update an existing entry by his [entryId] */ fun launchToUpdate(activity: Activity, database: Database, - entryId: NodeId) { + entryId: NodeId, + activityResultLauncher: ActivityResultLauncher) { if (database.loaded && !database.isReadOnly) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { val intent = Intent(activity, EntryEditActivity::class.java) intent.putExtra(KEY_ENTRY, entryId) - activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) + activityResultLauncher.launch(intent) } } } @@ -727,12 +770,13 @@ class EntryEditActivity : DatabaseLockActivity(), */ fun launchToCreate(activity: Activity, database: Database, - groupId: NodeId<*>) { + groupId: NodeId<*>, + activityResultLauncher: ActivityResultLauncher) { if (database.loaded && !database.isReadOnly) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { val intent = Intent(activity, EntryEditActivity::class.java) intent.putExtra(KEY_PARENT, groupId) - activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) + activityResultLauncher.launch(intent) } } } @@ -795,8 +839,9 @@ class EntryEditActivity : DatabaseLockActivity(), * Launch EntryEditActivity to add a new entry in autofill selection */ @RequiresApi(api = Build.VERSION_CODES.O) - fun launchForAutofillResult(activity: Activity, + fun launchForAutofillResult(activity: AppCompatActivity, database: Database, + activityResultLauncher: ActivityResultLauncher?, autofillComponent: AutofillComponent, groupId: NodeId<*>, searchInfo: SearchInfo? = null) { @@ -807,6 +852,7 @@ class EntryEditActivity : DatabaseLockActivity(), AutofillHelper.startActivityForAutofillResult( activity, intent, + activityResultLauncher, autofillComponent, searchInfo ) 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 5c0d740c1..446a9e127 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -31,8 +31,10 @@ import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.recyclerview.widget.LinearLayoutManager @@ -85,6 +87,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), private var mExternalFileHelper: ExternalFileHelper? = null + private var mAutofillActivityResultLauncher: ActivityResultLauncher? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + AutofillHelper.buildActivityResultLauncher(this) + else null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -109,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), // Open database button mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildOpenDocument { uri -> + uri?.let { + launchPasswordActivityWithPath(uri) + } + } + mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> + mDatabaseFileUri = databaseFileCreatedUri + if (mDatabaseFileUri != null) { + AssignMasterKeyDialogFragment.getInstance(true) + .show(supportFragmentManager, "passwordDialog") + } else { + val error = getString(R.string.error_create_database) + Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() + Log.e(TAG, error) + } + } openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) @@ -256,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), * Create a new file by calling the content provider */ private fun createNewFile() { - mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) + - getString(R.string.database_file_extension_default), "application/x-keepass") + mExternalFileHelper?.createDocument( + getString(R.string.database_file_name_default) + + getString(R.string.database_file_extension_default)) } private fun fileNoFoundAction(e: FileNotFoundException) { @@ -274,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), fileNoFoundAction(exception) }, { onCancelSpecialMode() }, - { onLaunchActivitySpecialMode() }) + { onLaunchActivitySpecialMode() }, + mAutofillActivityResultLauncher) } private fun launchGroupActivityIfLoaded(database: Database) { @@ -283,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), database, { onValidateSpecialMode() }, { onCancelSpecialMode() }, - { onLaunchActivitySpecialMode() }) + { onLaunchActivitySpecialMode() }, + mAutofillActivityResultLauncher) } } @@ -359,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) - } - - mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri -> - if (uri != null) { - launchPasswordActivityWithPath(uri) - } - } - - // Retrieve the created URI from the file manager - mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri -> - mDatabaseFileUri = databaseFileCreatedUri - if (mDatabaseFileUri != null) { - AssignMasterKeyDialogFragment.getInstance(true) - .show(supportFragmentManager, "passwordDialog") - } else { - val error = getString(R.string.error_create_database) - Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() - Log.e(TAG, error) - } - } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) @@ -499,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), */ @RequiresApi(api = Build.VERSION_CODES.O) - fun launchForAutofillResult(activity: Activity, + fun launchForAutofillResult(activity: AppCompatActivity, + activityResultLauncher: ActivityResultLauncher?, autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null) { AutofillHelper.startActivityForAutofillResult(activity, Intent(activity, FileDatabaseSelectActivity::class.java), + activityResultLauncher, autofillComponent, searchInfo) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 8c9bc7ee8..4abb99360 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -25,7 +25,7 @@ import android.app.TimePickerDialog import android.content.ComponentName import android.content.Context import android.content.Intent -import android.graphics.Color +import android.graphics.PorterDuff import android.os.* import android.util.Log import android.view.Menu @@ -33,18 +33,23 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.fragments.GroupFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity +import com.kunzisoft.keepass.adapters.BreadcrumbAdapter import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper @@ -58,6 +63,7 @@ import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.settings.PreferencesUtil @@ -75,6 +81,7 @@ class GroupActivity : DatabaseLockActivity(), GroupFragment.NodeClickListener, GroupFragment.NodesActionMenuListener, GroupFragment.OnScrollListener, + GroupFragment.GroupRefreshedListener, SortDialogFragment.SortSelectionListener { // Views @@ -82,18 +89,25 @@ class GroupActivity : DatabaseLockActivity(), private var coordinatorLayout: CoordinatorLayout? = null private var lockView: View? = null private var toolbar: Toolbar? = null + private var databaseNameContainer: ViewGroup? = null + private var databaseColorView: ImageView? = null + private var databaseNameView: TextView? = null + private var searchContainer: ViewGroup? = null + private var searchNumbers: TextView? = null + private var searchString: TextView? = null + private var toolbarBreadcrumb: Toolbar? = null private var searchTitleView: View? = null private var toolbarAction: ToolbarAction? = null - private var iconView: ImageView? = null private var numberChildrenView: TextView? = null private var addNodeButtonView: AddNodeButtonView? = null - private var groupNameView: TextView? = null - private var groupMetaView: TextView? = null + private var breadcrumbListView: RecyclerView? = null private var loadingView: ProgressBar? = null private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels() + private var mBreadcrumbAdapter: BreadcrumbAdapter? = null + private var mGroupFragment: GroupFragment? = null private var mRecyclingBinEnabled = false private var mRecyclingBinIsCurrentGroup = false @@ -111,7 +125,15 @@ class GroupActivity : DatabaseLockActivity(), private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null - private var mIconColor: Int = 0 + private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon -> + // To create tree dialog for icon + mGroupEditViewModel.selectIcon(icon) + } + + private var mAutofillActivityResultLauncher: ActivityResultLauncher? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + AutofillHelper.buildActivityResultLauncher(this) + else null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -122,13 +144,18 @@ class GroupActivity : DatabaseLockActivity(), // Initialize views rootContainerView = findViewById(R.id.activity_group_container_view) coordinatorLayout = findViewById(R.id.group_coordinator) - iconView = findViewById(R.id.group_icon) numberChildrenView = findViewById(R.id.group_numbers) addNodeButtonView = findViewById(R.id.add_node_button) toolbar = findViewById(R.id.toolbar) + databaseNameContainer = findViewById(R.id.database_name_container) + databaseColorView = findViewById(R.id.database_color) + databaseNameView = findViewById(R.id.database_name) + searchContainer = findViewById(R.id.search_container) + searchNumbers = findViewById(R.id.search_numbers) + searchString = findViewById(R.id.search_string) + toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb) searchTitleView = findViewById(R.id.search_title) - groupNameView = findViewById(R.id.group_name) - groupMetaView = findViewById(R.id.group_meta) + breadcrumbListView = findViewById(R.id.breadcrumb_list) toolbarAction = findViewById(R.id.toolbar_action) lockView = findViewById(R.id.lock_button) loadingView = findViewById(R.id.loading) @@ -140,10 +167,42 @@ class GroupActivity : DatabaseLockActivity(), toolbar?.title = "" setSupportActionBar(toolbar) - // Retrieve the textColor to tint the icon - val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) - mIconColor = taTextColor.getColor(0, Color.WHITE) - taTextColor.recycle() + mBreadcrumbAdapter = BreadcrumbAdapter(this).apply { + // Open group on breadcrumb click + onItemClickListener = { node, _ -> + // If last item & not a virtual root group + val currentGroup = mCurrentGroup + if (currentGroup != null && node == currentGroup + && (currentGroup != mDatabase?.rootGroup + || mDatabase?.rootGroupIsVirtual == false) + ) { + finishNodeAction() + launchDialogToShowGroupInfo(currentGroup) + } else { + if (mGroupFragment?.nodeActionSelectionMode == true) { + finishNodeAction() + } + mDatabase?.let { database -> + onNodeClick(database, node) + } + } + } + onLongItemClickListener = { node, position -> + val currentGroup = mCurrentGroup + if (currentGroup != null && node == currentGroup + && (currentGroup != mDatabase?.rootGroup + || mDatabase?.rootGroupIsVirtual == false) + ) { + finishNodeAction() + launchDialogForGroupUpdate(currentGroup) + } else { + onItemClickListener?.invoke(node, position) + } + } + } + breadcrumbListView?.apply { + adapter = mBreadcrumbAdapter + } // Retrieve group if defined at launch manageIntent(intent) @@ -201,21 +260,22 @@ class GroupActivity : DatabaseLockActivity(), // Add listeners to the add buttons addNodeButtonView?.setAddGroupClickListener { - GroupEditDialogFragment.create(GroupInfo().apply { - if (currentGroup.allowAddNoteInGroup) { - notes = "" - } - }).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) + launchDialogForGroupCreation(currentGroup) } addNodeButtonView?.setAddEntryClickListener { mDatabase?.let { database -> EntrySelectionHelper.doSpecialAction(intent, { - EntryEditActivity.launchToCreate( - this@GroupActivity, - database, - currentGroup.nodeId - ) + mCurrentGroup?.nodeId?.let { currentParentGroupId -> + mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher -> + EntryEditActivity.launchToCreate( + this@GroupActivity, + database, + currentParentGroupId, + resultLauncher + ) + } + } }, { // Search not used @@ -243,6 +303,7 @@ class GroupActivity : DatabaseLockActivity(), EntryEditActivity.launchForAutofillResult( this@GroupActivity, database, + mAutofillActivityResultLauncher, autofillComponent, currentGroup.nodeId, searchInfo @@ -266,9 +327,6 @@ class GroupActivity : DatabaseLockActivity(), } } - assignGroupViewElements(currentGroup) - invalidateOptionsMenu() - loadingView?.hideByFading() } @@ -277,7 +335,7 @@ class GroupActivity : DatabaseLockActivity(), } mGroupEditViewModel.requestIconSelection.observe(this) { iconImage -> - IconPickerActivity.launch(this@GroupActivity, iconImage) + IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher) } mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> @@ -319,6 +377,29 @@ class GroupActivity : DatabaseLockActivity(), return rootContainerView } + private fun loadGroup(database: Database?) { + when { + Intent.ACTION_SEARCH == intent.action -> { + finishNodeAction() + val searchString = + intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: "" + mGroupViewModel.loadGroupFromSearch( + database, + searchString, + PreferencesUtil.omitBackup(this) + ) + } + mCurrentGroupState == null -> { + mRootGroup?.let { rootGroup -> + mGroupViewModel.loadGroup(database, rootGroup, 0) + } + } + else -> { + mGroupViewModel.loadGroup(database, mCurrentGroupState) + } + } + } + override fun onDatabaseRetrieved(database: Database?) { super.onDatabaseRetrieved(database) @@ -328,17 +409,23 @@ class GroupActivity : DatabaseLockActivity(), && database?.isRecycleBinEnabled == true mRootGroup = database?.rootGroup - if (mCurrentGroupState == null) { - mRootGroup?.let { rootGroup -> - mGroupViewModel.loadGroup(database, rootGroup, 0) - } - } else { - mGroupViewModel.loadGroup(database, mCurrentGroupState) - } + loadGroup(database) // Search suggestion database?.let { + databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database) + val customColor = it.customColor + if (customColor != null) { + databaseColorView?.visibility = View.VISIBLE + databaseColorView?.setColorFilter( + customColor, + PorterDuff.Mode.SRC_IN + ) + } else { + databaseColorView?.visibility = View.GONE + } mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it) + mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory mOnSuggestionListener = object : SearchView.OnSuggestionListener { override fun onSuggestionClick(position: Int): Boolean { mSearchSuggestionAdapter?.let { searchAdapter -> @@ -413,16 +500,27 @@ class GroupActivity : DatabaseLockActivity(), ) } } + ACTION_DATABASE_UPDATE_GROUP_TASK -> { + if (result.isSuccess) { + try { + if (mCurrentGroup == newNodes[0] as Group) + reloadCurrentGroup() + } catch (e: Exception) { + Log.e( + TAG, + "Unable to perform action after group update", + e + ) + } + } + } } coordinatorLayout?.showActionErrorIfNeeded(result) if (!result.isSuccess) { reloadCurrentGroup() } - finishNodeAction() - - refreshNumberOfChildren(mCurrentGroup) } /** @@ -447,16 +545,7 @@ class GroupActivity : DatabaseLockActivity(), } // To transform KEY_SEARCH_INFO in ACTION_SEARCH transformSearchInfoIntent(intent) - if (Intent.ACTION_SEARCH == intent.action) { - finishNodeAction() - val searchString = - intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: "" - mGroupViewModel.loadGroupFromSearch( - mDatabase, - searchString, - PreferencesUtil.omitBackup(this) - ) - } + loadGroup(mDatabase) } } @@ -476,62 +565,44 @@ class GroupActivity : DatabaseLockActivity(), super.onSaveInstanceState(outState) } + override fun onGroupRefreshed() { + mCurrentGroup?.let { currentGroup -> + assignGroupViewElements(currentGroup) + } + } + private fun assignGroupViewElements(group: Group?) { // Assign title - if (group != null) { - if (groupNameView != null) { - val title = group.title - groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root) - groupNameView?.invalidate() - } - if (groupMetaView != null) { - val meta = group.nodeId.toString() - groupMetaView?.text = meta - if (meta.isNotEmpty() - && !group.isVirtual - && PreferencesUtil.showUUID(this)) { - groupMetaView?.visibility = View.VISIBLE - } else { - groupMetaView?.visibility = View.GONE - } - groupMetaView?.invalidate() - } - } - if (group?.isVirtual == true) { - searchTitleView?.visibility = View.VISIBLE - if (toolbar != null) { - toolbar?.navigationIcon = null - } - iconView?.visibility = View.GONE + searchContainer?.visibility = View.VISIBLE + val title = group.title + searchString?.text = if (title.isNotEmpty()) title else "" + searchNumbers?.text = group.numberOfChildEntries.toString() + databaseNameContainer?.visibility = View.GONE + toolbarBreadcrumb?.navigationIcon = null + toolbarBreadcrumb?.collapse() } else { - searchTitleView?.visibility = View.GONE - // Assign the group icon depending of IconPack or custom icon - iconView?.visibility = View.VISIBLE - group?.let { currentGroup -> - iconView?.let { imageView -> - mIconDrawableFactory?.assignDatabaseIcon( - imageView, - currentGroup.icon, - mIconColor - ) - } - - if (toolbar != null) { - if (group.containsParent()) - toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp) - else { - toolbar?.navigationIcon = null - } + searchContainer?.visibility = View.GONE + databaseNameContainer?.visibility = View.VISIBLE + // Refresh breadcrumb + if (toolbarBreadcrumb?.isVisible != true) { + toolbarBreadcrumb?.expand { + setBreadcrumbNode(group) } + } else { + // Add breadcrumb + setBreadcrumbNode(group) } } - - // Assign number of children - refreshNumberOfChildren(group) - - // Hide button initAddButton(group) + invalidateOptionsMenu() + } + + private fun setBreadcrumbNode(group: Group?) { + mBreadcrumbAdapter?.apply { + setNode(group) + breadcrumbListView?.scrollToPosition(itemCount -1) + } } private fun initAddButton(group: Group?) { @@ -553,18 +624,6 @@ class GroupActivity : DatabaseLockActivity(), } } - private fun refreshNumberOfChildren(group: Group?) { - numberChildrenView?.apply { - if (PreferencesUtil.showNumberEntries(context)) { - group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context)) - text = group?.numberOfChildEntries?.toString() ?: "" - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - } - override fun onScrolled(dy: Int) { if (actionNodeMode == null) addNodeButtonView?.hideOrShowButtonOnScrollListener(dy) @@ -594,11 +653,14 @@ class GroupActivity : DatabaseLockActivity(), val entryVersioned = node as Entry EntrySelectionHelper.doSpecialAction(intent, { - EntryActivity.launch( - this@GroupActivity, - database, - entryVersioned.nodeId - ) + mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher -> + EntryActivity.launch( + this@GroupActivity, + database, + entryVersioned.nodeId, + resultLauncher + ) + } }, { // Nothing here, a search is simply performed @@ -788,23 +850,42 @@ class GroupActivity : DatabaseLockActivity(), finishNodeAction() when (node.type) { Type.GROUP -> { - mOldGroupToUpdate = node as Group - GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo()) - .show( - supportFragmentManager, - GroupEditDialogFragment.TAG_CREATE_GROUP - ) + launchDialogForGroupUpdate(node as Group) + } + Type.ENTRY -> { + mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher -> + EntryEditActivity.launchToUpdate( + this@GroupActivity, + database, + (node as Entry).nodeId, + resultLauncher + ) + } } - Type.ENTRY -> EntryEditActivity.launchToUpdate( - this@GroupActivity, - database, - (node as Entry).nodeId - ) } reloadGroupIfSearch() return true } + private fun launchDialogToShowGroupInfo(group: Group) { + GroupDialogFragment.launch(group.getGroupInfo()) + .show(supportFragmentManager, GroupDialogFragment.TAG_SHOW_GROUP) + } + + private fun launchDialogForGroupCreation(group: Group) { + GroupEditDialogFragment.create(GroupInfo().apply { + if (group.allowAddNoteInGroup) { + notes = "" + } + }).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) + } + + private fun launchDialogForGroupUpdate(group: Group) { + mOldGroupToUpdate = group + GroupEditDialogFragment.update(group.getGroupInfo()) + .show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) + } + override fun onCopyMenuClick( database: Database, nodes: List @@ -888,10 +969,15 @@ class GroupActivity : DatabaseLockActivity(), inflater.inflate(R.menu.database, menu) if (mDatabaseReadOnly) { menu.findItem(R.id.menu_save_database)?.isVisible = false + menu.findItem(R.id.menu_merge_database)?.isVisible = false + } + if (!mMergeDataAllowed) { + menu.findItem(R.id.menu_merge_database)?.isVisible = false } if (mSpecialMode == SpecialMode.DEFAULT) { MenuUtil.defaultMenuInflater(inflater, menu) } else { + menu.findItem(R.id.menu_merge_database)?.isVisible = false menu.findItem(R.id.menu_reload_database)?.isVisible = false } @@ -984,7 +1070,7 @@ class GroupActivity : DatabaseLockActivity(), if (!sortMenuEducationPerformed) { // lockMenuEducationPerformed - val lockButtonView = findViewById(R.id.lock_button_icon) + val lockButtonView = findViewById(R.id.lock_button) lockButtonView != null && groupActivityEducation.checkAndPerformedLockMenuEducation( lockButtonView, @@ -1002,7 +1088,7 @@ class GroupActivity : DatabaseLockActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { - onBackPressed() + // TODO change database return true } R.id.menu_search -> @@ -1012,6 +1098,10 @@ class GroupActivity : DatabaseLockActivity(), saveDatabase() return true } + R.id.menu_merge_database -> { + mergeDatabase() + return true + } R.id.menu_reload_database -> { reloadDatabase() return true @@ -1057,37 +1147,6 @@ class GroupActivity : DatabaseLockActivity(), } } - override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { - /* - * ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in - * another app such as Files or GoogleDrive and then Search for an entry. Here we remove the - * FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task. - */ - if (Intent.ACTION_SEARCH == intent.action) { - var flags = intent.flags - flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() - intent.flags = flags - } - - super.startActivityForResult(intent, requestCode, options) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - // To create tree dialog for icon - IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> - mGroupEditViewModel.selectIcon(icon) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) - } - - // Directly used the onActivityResult in fragment - mGroupFragment?.onActivityResult(requestCode, resultCode, data) - } - private fun removeSearch() { intent.removeExtra(AUTO_SEARCH_KEY) if (Intent.ACTION_SEARCH == intent.action) { @@ -1292,8 +1351,9 @@ class GroupActivity : DatabaseLockActivity(), * ------------------------- */ @RequiresApi(api = Build.VERSION_CODES.O) - fun launchForAutofillResult(activity: Activity, + fun launchForAutofillResult(activity: AppCompatActivity, database: Database, + activityResultLaunch: ActivityResultLauncher?, autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null, autoSearch: Boolean = false) { @@ -1303,6 +1363,7 @@ class GroupActivity : DatabaseLockActivity(), AutofillHelper.startActivityForAutofillResult( activity, intent, + activityResultLaunch, autofillComponent, searchInfo ) @@ -1335,11 +1396,12 @@ class GroupActivity : DatabaseLockActivity(), * Global Launch * ------------------------- */ - fun launch(activity: Activity, + fun launch(activity: AppCompatActivity, database: Database, onValidateSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit, - onLaunchActivitySpecialMode: () -> Unit) { + onLaunchActivitySpecialMode: () -> Unit, + autofillActivityResultLauncher: ActivityResultLauncher?) { EntrySelectionHelper.doSpecialAction(activity.intent, { GroupActivity.launch( @@ -1451,6 +1513,7 @@ class GroupActivity : DatabaseLockActivity(), // Here no search info found, disable auto search GroupActivity.launchForAutofillResult(activity, database, + autofillActivityResultLauncher, autofillComponent, searchInfo, false) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt index ff9e7694e..93cf4b9a9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt @@ -27,9 +27,12 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.commit import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R @@ -82,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() { coordinatorLayout = findViewById(R.id.icon_picker_coordinator) mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildOpenDocument { uri -> + addCustomIcon(uri) + } uploadButton = findViewById(R.id.icon_picker_upload) @@ -309,14 +315,6 @@ class IconPickerActivity : DatabaseLockActivity() { ) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri -> - addCustomIcon(uri) - } - } - private fun setResult() { setResult(Activity.RESULT_OK, Intent().apply { putExtra(EXTRA_ICON, mIconImage) @@ -331,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() { companion object { private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG" - - private const val ICON_SELECTED_REQUEST = 15861 private const val EXTRA_ICON = "EXTRA_ICON" - private const val MAX_ICON_SIZE = 5242880 - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) { - if (requestCode == ICON_SELECTED_REQUEST) { - if (resultCode == Activity.RESULT_OK) { - listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage()) + fun registerIconSelectionForResult(context: FragmentActivity, + listener: (icon: IconImage) -> Unit): ActivityResultLauncher { + return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage()) } } } - fun launch(context: Activity, - previousIcon: IconImage?) { + fun launch(context: FragmentActivity, + previousIcon: IconImage?, + resultLauncher: ActivityResultLauncher) { // Create an instance to return the picker icon - context.startActivityForResult( - Intent(context, - IconPickerActivity::class.java).apply { + resultLauncher.launch( + Intent(context, IconPickerActivity::class.java).apply { if (previousIcon != null) putExtra(EXTRA_ICON, previousIcon) - }, - ICON_SELECTED_REQUEST) + } + ) } } } 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 e2973d645..79277962c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -35,12 +34,12 @@ import android.view.KeyEvent.KEYCODE_ENTER import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.InputMethodManager import android.widget.* -import android.widget.TextView.OnEditorActionListener +import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.app.ActivityCompat import androidx.fragment.app.commit import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R @@ -71,11 +70,12 @@ import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.asError +import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import java.io.FileNotFoundException -open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { +class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { // Views private var toolbar: Toolbar? = null @@ -89,7 +89,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui private lateinit var coordinatorLayout: CoordinatorLayout private var advancedUnlockFragment: AdvancedUnlockFragment? = null - private val databaseFileViewModel: DatabaseFileViewModel by viewModels() + private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels() + private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels() private var mDefaultDatabase: Boolean = false private var mDatabaseFileUri: Uri? = null @@ -98,20 +99,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui private var mRememberKeyFile: Boolean = false private var mExternalFileHelper: ExternalFileHelper? = null - private var mPermissionAsked = false private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false - set(value) { - infoContainerView?.visibility = if (value) { - mReadOnly = true - View.VISIBLE - } else { - View.GONE - } - field = value - } - private var mAllowAutoOpenBiometricPrompt: Boolean = true + private var mAutofillActivityResultLauncher: ActivityResultLauncher? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + AutofillHelper.buildActivityResultLauncher(this) + else null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -133,7 +127,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui infoContainerView = findViewById(R.id.activity_password_info_container) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) - mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) { savedInstanceState.getBoolean(KEY_READ_ONLY) } else { @@ -142,6 +135,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) + mExternalFileHelper?.buildOpenDocument { uri -> + if (uri != null) { + mDatabaseKeyFileUri = uri + populateKeyFileTextView(uri) + } + } keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) passwordView?.setOnEditorActionListener(onEditorActionListener) @@ -170,9 +169,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE)) } - if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) { - mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) - } // Init Biometric elements advancedUnlockFragment = supportFragmentManager @@ -188,21 +184,30 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui // Listen password checkbox to init advanced unlock and confirmation button checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> - advancedUnlockFragment?.checkUnlockAvailability() + mAdvancedUnlockViewModel.checkUnlockAvailability() enableOrNotTheConfirmationButton() } // Observe if default database - databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> + mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDefaultDatabase = isDefaultDatabase } // Observe database file change - databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> + mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> + // Force read only if the file does not exists - mForceReadOnly = databaseFile?.let { + val databaseFileNotExists = databaseFile?.let { !it.databaseFileExists } ?: true + infoContainerView?.visibility = if (databaseFileNotExists) { + mReadOnly = true + View.VISIBLE + } else { + View.GONE + } + mForceReadOnly = databaseFileNotExists + invalidateOptionsMenu() // Post init uri with KeyFile only if needed @@ -232,15 +237,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui } // Don't allow auto open prompt if lock become when UI visible - mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) - false - else - mAllowAutoOpenBiometricPrompt - mDatabaseFileUri?.let { databaseFileUri -> - databaseFileViewModel.loadDatabaseFile(databaseFileUri) + if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) { + mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false } - checkPermission() + mDatabaseFileUri?.let { databaseFileUri -> + mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) + } mDatabase?.let { database -> launchGroupActivityIfLoaded(database) @@ -263,7 +266,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui when (actionTask) { ACTION_DATABASE_LOAD_TASK -> { // Recheck advanced unlock if error - advancedUnlockFragment?.initAdvancedUnlockMode() + mAdvancedUnlockViewModel.initAdvancedUnlockMode() if (result.isSuccess) { launchGroupActivityIfLoaded(database) @@ -311,7 +314,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui is FileNotFoundDatabaseException -> { // Remove this default database inaccessible if (mDefaultDatabase) { - databaseFileViewModel.removeDefaultDatabase() + mDatabaseFileViewModel.removeDefaultDatabase() } } } @@ -344,7 +347,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) } mDatabaseFileUri?.let { - databaseFileViewModel.checkIfIsDefaultDatabase(it) + mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) } } @@ -361,7 +364,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui database, { onValidateSpecialMode() }, { onCancelSpecialMode() }, - { onLaunchActivitySpecialMode() } + { onLaunchActivitySpecialMode() }, + mAutofillActivityResultLauncher ) } } @@ -435,8 +439,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui verifyCheckboxesAndLoadDatabase(password, keyFileUri) } else { // Init Biometric elements - advancedUnlockFragment?.loadDatabase(databaseFileUri, - mAllowAutoOpenBiometricPrompt) + mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) } enableOrNotTheConfirmationButton() @@ -496,18 +499,15 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui override fun onPause() { // Reinit locking activity UI variable DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null - mAllowAutoOpenBiometricPrompt = true super.onPause() } override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked) mDatabaseKeyFileUri?.let { outState.putString(KEY_KEYFILE, it.toString()) } outState.putBoolean(KEY_READ_ONLY, mReadOnly) - outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false) super.onSaveInstanceState(outState) } @@ -606,35 +606,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui return true } - // Check permission - private fun checkPermission() { - if (Build.VERSION.SDK_INT in 23..28 - && !mReadOnly - && !mPermissionAsked) { - mPermissionAsked = true - // Check self permission to show or not the dialog - val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE - val permissions = arrayOf(writePermission) - if (toolbar != null - && ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST) - } - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - when (requestCode) { - WRITE_EXTERNAL_STORAGE_REQUEST -> { - if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { - if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) - Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show() - } - } - } - } - // To fix multiple view education private var performedEductionInProgress = false private fun launchEducation(menu: Menu) { @@ -709,45 +680,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui return super.onOptionsItemSelected(item) } - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - mAllowAutoOpenBiometricPrompt = false - - // To get device credential unlock result - advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data) - - // To get entry in result - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) - } - - var keyFileResult = false - mExternalFileHelper?.let { - keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri -> - if (uri != null) { - mDatabaseKeyFileUri = uri - populateKeyFileTextView(uri) - } - } - } - if (!keyFileResult) { - // this block if not a key file response - when (resultCode) { - DatabaseLockActivity.RESULT_EXIT_LOCK -> { - clearCredentialsViews() - closeDatabase() - } - Activity.RESULT_CANCELED -> { - clearCredentialsViews() - } - } - } - } - companion object { private val TAG = PasswordActivity::class.java.name @@ -761,10 +693,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_PASSWORD = "password" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" - private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED" - private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647 - - private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT" private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, intentBuildLauncher: (Intent) -> Unit) { @@ -855,15 +783,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui @RequiresApi(api = Build.VERSION_CODES.O) @Throws(FileNotFoundException::class) - fun launchForAutofillResult(activity: Activity, + fun launchForAutofillResult(activity: AppCompatActivity, databaseFile: Uri, keyFile: Uri?, + activityResultLauncher: ActivityResultLauncher?, autofillComponent: AutofillComponent, searchInfo: SearchInfo?) { buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> AutofillHelper.startActivityForAutofillResult( activity, intent, + activityResultLauncher, autofillComponent, searchInfo) } @@ -891,12 +821,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui * Global Launch * ------------------------- */ - fun launch(activity: Activity, + fun launch(activity: AppCompatActivity, databaseUri: Uri, keyFile: Uri?, fileNoFoundAction: (exception: FileNotFoundException) -> Unit, onCancelSpecialMode: () -> Unit, - onLaunchActivitySpecialMode: () -> Unit) { + onLaunchActivitySpecialMode: () -> Unit, + autofillActivityResultLauncher: ActivityResultLauncher?) { try { EntrySelectionHelper.doSpecialAction(activity.intent, @@ -926,6 +857,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PasswordActivity.launchForAutofillResult(activity, databaseUri, keyFile, + autofillActivityResultLauncher, autofillComponent, searchInfo) onLaunchActivitySpecialMode() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index 13c3a9d60..b976f1f54 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog import android.content.Context import android.content.DialogInterface -import android.content.Intent import android.net.Uri import android.os.Bundle import android.text.Editable @@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildOpenDocument { uri -> + uri?.let { pathUri -> + UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile -> + keyFileSelectionView?.error = null + keyFileCheckBox?.isChecked = true + keyFileSelectionView?.uri = pathUri + if (lengthFile <= 0L) { + showEmptyKeyFileConfirmationDialog() + } + } + } + } keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) val dialog = builder.create() @@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match) } - if (mMasterPassword == null || mMasterPassword!!.isEmpty()) { + if ((mMasterPassword == null + || mMasterPassword!!.isEmpty()) + && (keyFileCheckBox == null + || !keyFileCheckBox!!.isChecked + || keyFileSelectionView?.uri == null)) { error = true showEmptyPasswordConfirmationDialog() } @@ -282,23 +297,6 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri -> - uri?.let { pathUri -> - UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile -> - keyFileSelectionView?.error = null - keyFileCheckBox?.isChecked = true - keyFileSelectionView?.uri = pathUri - if (lengthFile <= 0L) { - showEmptyKeyFileConfirmationDialog() - } - } - } - } - } - companion object { private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ColorPickerDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ColorPickerDialogFragment.kt new file mode 100644 index 000000000..2b9e017d3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ColorPickerDialogFragment.kt @@ -0,0 +1,95 @@ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Dialog +import android.graphics.Color +import android.os.Bundle +import android.widget.CompoundButton +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.activityViewModels +import com.kunzisoft.androidclearchroma.view.ChromaColorView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel + +class ColorPickerDialogFragment : DatabaseDialogFragment() { + + private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels() + + private lateinit var enableSwitchView: CompoundButton + private lateinit var chromaColorView: ChromaColorView + + private var mDefaultColor = Color.WHITE + private var mActivated = false + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + activity?.let { activity -> + val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null) + enableSwitchView = root.findViewById(R.id.switch_element) + chromaColorView = root.findViewById(R.id.chroma_color_view) + + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) { + mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR) + } + if (savedInstanceState.containsKey(ARG_ACTIVATED)) { + mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED) + } + } else { + arguments?.apply { + if (containsKey(ARG_INITIAL_COLOR)) { + mDefaultColor = getInt(ARG_INITIAL_COLOR) + } + if (containsKey(ARG_ACTIVATED)) { + mActivated = getBoolean(ARG_ACTIVATED) + } + } + } + enableSwitchView.isChecked = mActivated + chromaColorView.currentColor = mDefaultColor + + chromaColorView.setOnColorChangedListener { + if (!enableSwitchView.isChecked) + enableSwitchView.isChecked = true + } + + val builder = AlertDialog.Builder(activity) + builder.setView(root) + .setPositiveButton(android.R.string.ok) { _, _ -> + val color: Int? = if (enableSwitchView.isChecked) + chromaColorView.currentColor + else + null + mColorPickerViewModel.pickColor(color) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // Do nothing + } + + return builder.create() + } + return super.onCreateDialog(savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor) + outState.putBoolean(ARG_ACTIVATED, mActivated) + } + + companion object { + private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR" + private const val ARG_ACTIVATED = "ARG_ACTIVATED" + + fun newInstance( + @ColorInt initialColor: Int?, + ): ColorPickerDialogFragment { + return ColorPickerDialogFragment().apply { + arguments = Bundle().apply { + putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE) + putBoolean(ARG_ACTIVATED, initialColor != null) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt index 1cacb3d3c..fc3708205 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt @@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { } } + @Suppress("DEPRECATION") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt new file mode 100644 index 000000000..1038df7fe --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2021 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.activities.dialogs + +import android.app.Dialog +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.icon.IconImage +import com.kunzisoft.keepass.model.GroupInfo +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.UuidUtil +import com.kunzisoft.keepass.view.DateTimeFieldView + +class GroupDialogFragment : DatabaseDialogFragment() { + + private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null + private var mGroupInfo = GroupInfo() + + private lateinit var iconView: ImageView + private var mIconColor: Int = 0 + private lateinit var nameTextView: TextView + private lateinit var notesTextLabelView: TextView + private lateinit var notesTextView: TextView + private lateinit var expirationView: DateTimeFieldView + private lateinit var creationView: TextView + private lateinit var modificationView: TextView + private lateinit var uuidContainerView: ViewGroup + private lateinit var uuidReferenceView: TextView + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + mPopulateIconMethod = { imageView, icon -> + database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) + } + mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + val root = activity.layoutInflater.inflate(R.layout.fragment_group, null) + iconView = root.findViewById(R.id.group_icon) + nameTextView = root.findViewById(R.id.group_name) + notesTextLabelView = root.findViewById(R.id.group_note_label) + notesTextView = root.findViewById(R.id.group_note) + expirationView = root.findViewById(R.id.group_expiration) + creationView = root.findViewById(R.id.group_created) + modificationView = root.findViewById(R.id.group_modified) + uuidContainerView = root.findViewById(R.id.group_UUID_container) + uuidReferenceView = root.findViewById(R.id.group_UUID_reference) + + // Retrieve the textColor to tint the icon + val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) + mIconColor = ta.getColor(0, Color.WHITE) + ta.recycle() + + if (savedInstanceState != null + && savedInstanceState.containsKey(KEY_GROUP_INFO)) { + mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo + } else { + arguments?.apply { + if (containsKey(KEY_GROUP_INFO)) { + mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo + } + } + } + + // populate info in views + val title = mGroupInfo.title + if (title.isEmpty()) { + nameTextView.visibility = View.GONE + } else { + nameTextView.text = title + nameTextView.visibility = View.VISIBLE + } + val notes = mGroupInfo.notes + if (notes == null || notes.isEmpty()) { + notesTextLabelView.visibility = View.GONE + notesTextView.visibility = View.GONE + } else { + notesTextView.text = notes + notesTextLabelView.visibility = View.VISIBLE + notesTextView.visibility = View.VISIBLE + } + expirationView.activation = mGroupInfo.expires + expirationView.dateTime = mGroupInfo.expiryTime + creationView.text = mGroupInfo.creationTime.getDateTimeString(resources) + modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources) + val uuid = UuidUtil.toHexString(mGroupInfo.id) + if (uuid == null || uuid.isEmpty()) { + uuidContainerView.visibility = View.GONE + } else { + uuidReferenceView.text = uuid + uuidContainerView.apply { + visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE + } + } + + val builder = AlertDialog.Builder(activity) + builder.setView(root) + .setPositiveButton(android.R.string.ok){ _, _ -> + // Do nothing + } + return builder.create() + } + return super.onCreateDialog(savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(KEY_GROUP_INFO, mGroupInfo) + super.onSaveInstanceState(outState) + } + + data class Error(val isError: Boolean, val messageId: Int?) + + companion object { + const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP" + private const val KEY_GROUP_INFO = "KEY_GROUP_INFO" + + fun launch(groupInfo: GroupInfo): GroupDialogFragment { + val bundle = Bundle() + bundle.putParcelable(KEY_GROUP_INFO, groupInfo) + val fragment = GroupDialogFragment() + fragment.arguments = bundle + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt index bb3ffe39c..482bdb05e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt @@ -246,8 +246,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() { companion object { const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" - const val KEY_ACTION_ID = "KEY_ACTION_ID" - const val KEY_GROUP_INFO = "KEY_GROUP_INFO" + private const val KEY_ACTION_ID = "KEY_ACTION_ID" + private const val KEY_GROUP_INFO = "KEY_GROUP_INFO" fun create(groupInfo: GroupInfo): GroupEditDialogFragment { val bundle = Bundle() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt index 77b18d107..26c769afb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt @@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() { override fun afterTextChanged(s: Editable?) { s?.toString()?.let { userString -> try { - mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH)) + mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH)) otpSecretContainer?.error = null } catch (exception: Exception) { otpSecretContainer?.error = getString(R.string.error_otp_secret_key) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt index 4fd4b3905..a2decd806 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt @@ -109,6 +109,12 @@ class EntryEditFragment: DatabaseFragment() { setOnIconClickListener { mEntryEditViewModel.requestIconSelection(templateView.getIcon()) } + setOnBackgroundColorClickListener { + mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor()) + } + setOnForegroundColorClickListener { + mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor()) + } setOnCustomEditionActionClickListener { field -> mEntryEditViewModel.requestCustomFieldEdition(field) } @@ -158,6 +164,14 @@ class EntryEditFragment: DatabaseFragment() { templateView.setIcon(iconImage) } + mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color -> + templateView.setBackgroundColor(color) + } + + mEntryEditViewModel.onForegroundColorSelected.observe(this) { color -> + templateView.setForegroundColor(color) + } + mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField -> templateView.setPasswordField(passwordField) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt index 00e89a9a2..0e9e9ca2e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt @@ -42,7 +42,6 @@ class EntryFragment: DatabaseFragment() { private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private lateinit var uuidContainerView: View - private lateinit var uuidView: TextView private lateinit var uuidReferenceView: TextView private var mClipboardHelper: ClipboardHelper? = null @@ -88,7 +87,6 @@ class EntryFragment: DatabaseFragment() { uuidContainerView.apply { visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE } - uuidView = view.findViewById(R.id.entry_UUID) uuidReferenceView = view.findViewById(R.id.entry_UUID_reference) mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory -> @@ -200,7 +198,6 @@ class EntryFragment: DatabaseFragment() { } private fun assignUUID(uuid: UUID?) { - uuidView.text = uuid?.toString() uuidReferenceView.text = UuidUtil.toHexString(uuid) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt index 39d5b3b86..f7d857838 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities.fragments import android.content.Context -import android.content.Intent import android.os.Bundle import android.util.Log import android.view.* @@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.adapters.NodeAdapter +import com.kunzisoft.keepass.adapters.NodesAdapter import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.node.Node -import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable @@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen private var nodeClickListener: NodeClickListener? = null private var onScrollListener: OnScrollListener? = null + private var groupRefreshed: GroupRefreshedListener? = null private var mNodesRecyclerView: RecyclerView? = null private var mLayoutManager: LinearLayoutManager? = null - private var mAdapter: NodeAdapter? = null + private var mAdapter: NodesAdapter? = null private val mGroupViewModel: GroupViewModel by activityViewModels() @@ -74,6 +73,19 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen private var mRecycleBinEnable: Boolean = false private var mRecycleBin: Group? = null + var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId -> + entryId?.let { + // Simply refresh the list + rebuildList() + // Scroll to the new entry + mDatabase?.getEntryById(it)?.let { entry -> + mAdapter?.indexOf(entry)?.let { position -> + mNodesRecyclerView?.scrollToPosition(position) + } + } + } ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result") + } + private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) @@ -89,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen override fun onAttach(context: Context) { super.onAttach(context) + + // TODO Change to ViewModel try { nodeClickListener = context as NodeClickListener } catch (e: ClassCastException) { // The activity doesn't implement the interface, throw exception throw ClassCastException(context.toString() - + " must implement " + NodeAdapter.NodeClickCallback::class.java.name) + + " must implement " + NodesAdapter.NodeClickCallback::class.java.name) } try { @@ -102,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen } catch (e: ClassCastException) { onScrollListener = null // Context menu can be omit - Log.w(TAG, context.toString() + Log.w( + TAG, context.toString() + " must implement " + RecyclerView.OnScrollListener::class.java.name) } + + try { + groupRefreshed = context as GroupRefreshedListener + } catch (e: ClassCastException) { + // The activity doesn't implement the interface, throw exception + throw ClassCastException(context.toString() + + " must implement " + GroupRefreshedListener::class.java.name) + } } override fun onDetach() { nodeClickListener = null onScrollListener = null + groupRefreshed = null super.onDetach() } @@ -125,8 +149,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen contextThemed?.let { context -> database?.let { database -> - mAdapter = NodeAdapter(context, database).apply { - setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { + mAdapter = NodesAdapter(context, database).apply { + setOnNodeClickListener(object : NodesAdapter.NodeClickCallback { override fun onNodeClick(database: Database, node: Node) { if (nodeActionSelectionMode) { if (listActionNodes.contains(node)) { @@ -182,7 +206,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen super.onCreateView(inflater, container, savedInstanceState) // To apply theme return inflater.cloneInContext(contextThemed) - .inflate(R.layout.fragment_group, container, false) + .inflate(R.layout.fragment_nodes, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -247,6 +271,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen } else { notFoundView?.visibility = View.GONE } + + groupRefreshed?.onGroupRefreshed() } override fun onSortSelected(sortNodeEnum: SortNodeEnum, @@ -279,15 +305,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen val sortDialogFragment: SortDialogFragment = if (mRecycleBinEnable) { SortDialogFragment.getInstance( - PreferencesUtil.getListSort(context), - PreferencesUtil.getAscendingSort(context), - PreferencesUtil.getGroupsBeforeSort(context), - PreferencesUtil.getRecycleBinBottomSort(context)) + PreferencesUtil.getListSort(context), + PreferencesUtil.getAscendingSort(context), + PreferencesUtil.getGroupsBeforeSort(context), + PreferencesUtil.getRecycleBinBottomSort(context) + ) } else { SortDialogFragment.getInstance( - PreferencesUtil.getListSort(context), - PreferencesUtil.getAscendingSort(context), - PreferencesUtil.getGroupsBeforeSort(context)) + PreferencesUtil.getListSort(context), + PreferencesUtil.getAscendingSort(context), + PreferencesUtil.getGroupsBeforeSort(context) + ) } sortDialogFragment.show(childFragmentManager, "sortDialog") @@ -399,27 +427,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - when (requestCode) { - EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { - if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) { - data?.getParcelableExtra>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { - // Simply refresh the list - rebuildList() - // Scroll to the new entry - mDatabase?.getEntryById(it)?.let { entry -> - mAdapter?.indexOf(entry)?.let { position -> - mNodesRecyclerView?.scrollToPosition(position) - } - } - } ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result") - } - } - } - } - /** * Callback listener to redefine to do an action when a node is click */ @@ -455,6 +462,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen fun onScrolled(dy: Int) } + interface GroupRefreshedListener { + fun onGroupRefreshed() + } + companion object { private val TAG = GroupFragment::class.java.name } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt index 2792c35ef..cc12c2347 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt @@ -20,14 +20,16 @@ package com.kunzisoft.keepass.activities.helpers import android.annotation.SuppressLint -import android.app.Activity.RESULT_OK +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.util.Log import android.view.View -import androidx.annotation.RequiresApi +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment @@ -38,6 +40,10 @@ class ExternalFileHelper { private var activity: FragmentActivity? = null private var fragment: Fragment? = null + private var getContentResultLauncher: ActivityResultLauncher? = null + private var openDocumentResultLauncher: ActivityResultLauncher>? = null + private var createDocumentResultLauncher: ActivityResultLauncher? = null + constructor(context: FragmentActivity) { this.activity = context this.fragment = null @@ -48,94 +54,81 @@ class ExternalFileHelper { this.fragment = context } + fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) { + + val resultCallback = ActivityResultCallback { result -> + result?.let { uri -> + UriUtil.takeUriPermission(activity?.contentResolver, uri) + onFileSelected?.invoke(uri) + } + } + + getContentResultLauncher = if (fragment != null) { + fragment?.registerForActivityResult( + GetContent(), + resultCallback + ) + } else { + activity?.registerForActivityResult( + GetContent(), + resultCallback + ) + } + + openDocumentResultLauncher = if (fragment != null) { + fragment?.registerForActivityResult( + OpenDocument(), + resultCallback + ) + } else { + activity?.registerForActivityResult( + OpenDocument(), + resultCallback + ) + } + } + + fun buildCreateDocument(typeString: String = "application/octet-stream", + onFileCreated: (fileCreated: Uri?)->Unit) { + + val resultCallback = ActivityResultCallback { result -> + onFileCreated.invoke(result) + } + + createDocumentResultLauncher = if (fragment != null) { + fragment?.registerForActivityResult( + CreateDocument(typeString), + resultCallback + ) + } else { + activity?.registerForActivityResult( + CreateDocument(typeString), + resultCallback + ) + } + } + fun openDocument(getContent: Boolean = false, typeString: String = "*/*") { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - try { - if (getContent) { - openActivityWithActionGetContent(typeString) - } else { - openActivityWithActionOpenDocument(typeString) - } - } catch (e: Exception) { - Log.e(TAG, "Unable to open document", e) - showFileManagerDialogFragment() + try { + if (getContent) { + getContentResultLauncher?.launch(typeString) + } else { + openDocumentResultLauncher?.launch(arrayOf(typeString)) } - } else { + } catch (e: Exception) { + Log.e(TAG, "Unable to open document", e) showFileManagerDialogFragment() } } - @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun openActivityWithActionOpenDocument(typeString: String) { - val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = typeString - addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) - } - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + fun createDocument(titleString: String) { + try { + createDocumentResultLauncher?.launch(titleString) + } catch (e: Exception) { + Log.e(TAG, "Unable to create document", e) + showFileManagerDialogFragment() } - if (fragment != null) - fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC) - else - activity?.startActivityForResult(intentOpenDocument, OPEN_DOC) - } - - @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun openActivityWithActionGetContent(typeString: String) { - val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = typeString - addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) - } - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - } - if (fragment != null) - fragment?.startActivityForResult(intentGetContent, GET_CONTENT) - else - activity?.startActivityForResult(intentGetContent, GET_CONTENT) - } - - /** - * To use in onActivityResultCallback in Fragment or Activity - * @param onFileSelected Callback retrieve from data - * @return true if requestCode was captured, false elsewhere - */ - fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?, - onFileSelected: ((uri: Uri?) -> Unit)?): Boolean { - - when (requestCode) { - FILE_BROWSE -> { - if (resultCode == RESULT_OK) { - val filename = data?.dataString - var keyUri: Uri? = null - if (filename != null) { - keyUri = UriUtil.parse(filename) - } - onFileSelected?.invoke(keyUri) - } - return true - } - GET_CONTENT, OPEN_DOC -> { - if (resultCode == RESULT_OK) { - if (data != null) { - val uri = data.data - if (uri != null) { - UriUtil.takeUriPermission(activity?.contentResolver, uri) - onFileSelected?.invoke(uri) - } - } - } - return true - } - } - return false } /** @@ -155,62 +148,50 @@ class ExternalFileHelper { } } - fun createDocument(titleString: String, - typeString: String = "application/octet-stream"): Int? { - val idCode = getUnusedCreateFileRequestCode() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - try { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = typeString - putExtra(Intent.EXTRA_TITLE, titleString) + class OpenDocument : ActivityResultContracts.OpenDocument() { + @SuppressLint("InlinedApi") + override fun createIntent(context: Context, input: Array): Intent { + return super.createIntent(context, input).apply { + addCategory(Intent.CATEGORY_OPENABLE) + addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) } - if (fragment != null) - fragment?.startActivityForResult(intent, idCode) - else - activity?.startActivityForResult(intent, idCode) - return idCode - } catch (e: Exception) { - Log.e(TAG, "Unable to create document", e) - showFileManagerDialogFragment() + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } - } else { - showFileManagerDialogFragment() } - return null } - /** - * To use in onActivityResultCallback in Fragment or Activity - * @param onFileCreated Callback retrieve from data - * @return true if requestCode was captured, false elsewhere - */ - fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?, - onFileCreated: (fileCreated: Uri?)->Unit) { - // Retrieve the created URI from the file manager - if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) { - onFileCreated.invoke(data?.data) - fileRequestCodes.remove(requestCode) + class GetContent : ActivityResultContracts.GetContent() { + @SuppressLint("InlinedApi") + override fun createIntent(context: Context, input: String): Intent { + return super.createIntent(context, input).apply { + addCategory(Intent.CATEGORY_OPENABLE) + addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) + } + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } } } + class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() { + override fun createIntent(context: Context, input: String): Intent { + return super.createIntent(context, input).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = typeString + } + } + } + + companion object { private const val TAG = "OpenFileHelper" - private const val GET_CONTENT = 25745 - private const val OPEN_DOC = 25845 - private const val FILE_BROWSE = 25645 - - private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853 - private var fileRequestCodes = ArrayList() - - private fun getUnusedCreateFileRequestCode(): Int { - val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++ - fileRequestCodes.add(newCreateFileRequestCode) - return newCreateFileRequestCode - } - @SuppressLint("InlinedApi") fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager, typeString: String = "application/octet-stream"): Boolean { @@ -231,7 +212,7 @@ class ExternalFileHelper { fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) { externalFileHelper?.let { fileHelper -> setOnClickListener { - fileHelper.openDocument() + fileHelper.openDocument(false) } setOnLongClickListener { fileHelper.openDocument(true) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index b4d4d7a9f..6b5e79d64 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), private var mExitLock: Boolean = false protected var mDatabaseReadOnly: Boolean = true + protected var mMergeDataAllowed: Boolean = false private var mAutoSaveEnable: Boolean = true protected var mIconDrawableFactory: IconDrawableFactory? = null @@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(save) } + mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid -> + mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid) + } + mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> - mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) + mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) { + mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) + } } mDatabaseViewModel.saveName.observe(this) { @@ -100,7 +107,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } mDatabaseViewModel.saveDefaultUsername.observe(this) { - mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save) + mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save) } mDatabaseViewModel.saveColor.observe(this) { @@ -180,8 +187,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), closeDatabase(database) if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null) LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE - // Add onActivityForResult response - setResult(RESULT_EXIT_LOCK) + mExitLock = true closeOptionsMenu() finish() } @@ -198,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } mDatabaseReadOnly = database.isReadOnly + mMergeDataAllowed = database.isMergeDataAllowed() mIconDrawableFactory = database.iconDrawableFactory checkRegister() @@ -213,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), ) { super.onDatabaseActionFinished(database, actionTask, result) when (actionTask) { + DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK, DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { // Reload the current activity if (result.isSuccess) { @@ -255,8 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(true) } + fun mergeDatabase() { + mDatabaseTaskProvider?.startDatabaseMerge(false) + } + fun reloadDatabase() { - mDatabaseTaskProvider?.startDatabaseReload(false) + mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) { + mDatabaseTaskProvider?.startDatabaseReload(false) + } } fun createEntry(newEntry: Entry, @@ -353,14 +367,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode == RESULT_EXIT_LOCK) { - mExitLock = true - lockAndExit() - } - } - private fun checkRegister() { // If in ave or registration mode, don't allow read only if ((mSpecialMode == SpecialMode.SAVE @@ -440,8 +446,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), const val TAG = "LockingActivity" - const val RESULT_EXIT_LOCK = 1450 - const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" const val TIMEOUT_ENABLE_KEY_DEFAULT = true diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt index 71ba57311..9972dc227 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt @@ -39,7 +39,11 @@ object Stylish { */ fun load(context: Context) { Log.d(Stylish::class.java.name, "Attatching to " + context.packageName) - themeString = PreferencesUtil.getStyle(context) + try { + themeString = PreferencesUtil.getStyle(context) + } catch (e: Exception) { + Log.e("Stylish", "Unable to get preference style", e) + } } fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt index 15f76e4f6..32904c342 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt @@ -28,7 +28,7 @@ import android.util.Log import android.view.WindowManager import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatActivity -import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED +import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED /** * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from @@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() { super.onResume() if ((customStyle && Stylish.getThemeId(this) != this.themeId) - || DATABASE_APPEARANCE_PREFERENCE_CHANGED) { - DATABASE_APPEARANCE_PREFERENCE_CHANGED = false + || DATABASE_PREFERENCE_CHANGED) { + DATABASE_PREFERENCE_CHANGED = false Log.d(this.javaClass.name, "Theme change detected, restarting activity") recreateActivity() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt index 8d9aef137..ddb69d1c6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt @@ -23,12 +23,12 @@ import android.content.Context import android.graphics.Color import android.os.Build import android.os.Bundle -import androidx.annotation.StyleRes -import androidx.fragment.app.Fragment -import androidx.appcompat.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.StyleRes +import androidx.appcompat.view.ContextThemeWrapper +import androidx.fragment.app.Fragment abstract class StylishFragment : Fragment() { @@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() { contextThemed = ContextThemeWrapper(context, themeId) } - @Suppress("DEPRECATION") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // To fix status bar color if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() { try { val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) if (taWindowStatusLight?.getBoolean(0, false) == true) { + @Suppress("DEPRECATION") window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } taWindowStatusLight?.recycle() diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt new file mode 100644 index 000000000..b3073a9c0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt @@ -0,0 +1,150 @@ +package com.kunzisoft.keepass.adapters + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.icons.IconDrawableFactory +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.view.strikeOut + +class BreadcrumbAdapter(val context: Context) + : RecyclerView.Adapter() { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + var iconDrawableFactory: IconDrawableFactory? = null + @SuppressLint("NotifyDataSetChanged") + set(value) { + field = value + notifyDataSetChanged() + } + private var mNodeBreadcrumb: MutableList = mutableListOf() + var onItemClickListener: ((item: Node, position: Int)->Unit)? = null + var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null + + private var mShowNumberEntries = false + private var mShowUUID = false + private var mIconColor: Int = 0 + + init { + mShowNumberEntries = PreferencesUtil.showNumberEntries(context) + mShowUUID = PreferencesUtil.showUUID(context) + + // Retrieve the textColor to tint the icon + val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + mIconColor = taTextColor.getColor(0, Color.WHITE) + taTextColor.recycle() + } + + @SuppressLint("NotifyDataSetChanged") + fun setNode(node: Node?) { + mNodeBreadcrumb.clear() + node?.let { + var currentNode = it + mNodeBreadcrumb.add(0, currentNode) + while (currentNode.containsParent()) { + currentNode.parent?.let { parent -> + currentNode = parent + mNodeBreadcrumb.add(0, currentNode) + } + } + } + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int { + return when (position) { + mNodeBreadcrumb.size - 1 -> 0 + else -> 1 + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder { + return BreadcrumbGroupViewHolder(inflater.inflate( + when (viewType) { + 0 -> R.layout.item_group + else -> R.layout.item_breadcrumb + }, parent, false) + ) + } + + override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) { + val node = mNodeBreadcrumb[position] + + holder.groupNameView.apply { + text = node?.title ?: "" + strikeOut(node?.isCurrentlyExpires ?: false) + } + + holder.itemView.apply { + setOnClickListener { + node?.let { + onItemClickListener?.invoke(it, position) + } + } + setOnLongClickListener { + node?.let { + onLongItemClickListener?.invoke(it, position) + } + true + } + } + + if (node?.type == Type.GROUP) { + (node as Group).let { group -> + + holder.groupIconView?.let { imageView -> + iconDrawableFactory?.assignDatabaseIcon( + imageView, + group.icon, + mIconColor + ) + } + + holder.groupNumbersView?.apply { + if (mShowNumberEntries) { + group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context)) + text = group.numberOfChildEntries.toString() + visibility = View.VISIBLE + } else { + visibility = View.GONE + } + } + + holder.groupMetaView?.apply { + val meta = group.nodeId.toVisualString() + visibility = if (meta != null + && !group.isVirtual + && mShowUUID + ) { + text = meta + View.VISIBLE + } else { + View.GONE + } + } + } + } + + } + + override fun getItemCount(): Int { + return mNodeBreadcrumb.size + } + + inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon) + var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers) + var groupNameView: TextView = itemView.findViewById(R.id.group_name) + var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt similarity index 78% rename from app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt rename to app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt index e7b1164be..6f3a9b162 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt @@ -26,7 +26,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast import androidx.annotation.ColorInt @@ -34,6 +33,7 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedListAdapterCallback +import com.google.android.material.progressindicator.CircularProgressIndicator import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry @@ -55,9 +55,9 @@ import java.util.* * Create node list adapter with contextMenu or not * @param context Context to use */ -class NodeAdapter (private val context: Context, - private val database: Database) - : RecyclerView.Adapter() { +class NodesAdapter (private val context: Context, + private val database: Database) + : RecyclerView.Adapter() { private var mNodeComparator: Comparator>? = null private val mNodeSortedListCallback: NodeSortedListCallback @@ -79,6 +79,8 @@ class NodeAdapter (private val context: Context, private var mShowOTP: Boolean = false private var mShowUUID: Boolean = false private var mEntryFilters = arrayOf() + private var mOldVirtualGroup = false + private var mVirtualGroup = false private var mActionNodesList = LinkedList() private var mNodeClickCallback: NodeClickCallback? = null @@ -87,9 +89,15 @@ class NodeAdapter (private val context: Context, @ColorInt private val mContentSelectionColor: Int @ColorInt - private val mIconGroupColor: Int + private val mTextColorPrimary: Int @ColorInt - private val mIconEntryColor: Int + private val mTextColor: Int + @ColorInt + private val mTextColorSecondary: Int + @ColorInt + private val mColorAccentLight: Int + @ColorInt + private val mTextColorSelected: Int /** * Determine if the adapter contains or not any element @@ -110,12 +118,24 @@ class NodeAdapter (private val context: Context, this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) // Retrieve the color to tint the icon val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) - this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) + this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK) taTextColorPrimary.recycle() - // In two times to fix bug compilation + // To get text color val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) - this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK) + this.mTextColor = taTextColor.getColor(0, Color.BLACK) taTextColor.recycle() + // To get text color secondary + val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary)) + this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK) + taTextColorSecondary.recycle() + // To get background color for selection + val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight)) + this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY) + taSelectionColor.recycle() + // To get text color for selection + val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor)) + this.mTextColorSelected = taSelectionTextColor.getColor(0, Color.WHITE) + taSelectionTextColor.recycle() } private fun assignPreferences() { @@ -145,6 +165,8 @@ class NodeAdapter (private val context: Context, * Rebuild the list by clear and build children from the group */ fun rebuildList(group: Group) { + mOldVirtualGroup = mVirtualGroup + mVirtualGroup = group.isVirtual assignPreferences() mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters)) } @@ -155,14 +177,19 @@ class NodeAdapter (private val context: Context, } override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { + if (mOldVirtualGroup != mVirtualGroup) + return false var typeContentTheSame = true if (oldItem is Entry && newItem is Entry) { typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle() && oldItem.username == newItem.username + && oldItem.backgroundColor == newItem.backgroundColor + && oldItem.foregroundColor == newItem.foregroundColor && oldItem.getOtpElement() == newItem.getOtpElement() && oldItem.containsAttachment() == newItem.containsAttachment() } else if (oldItem is Group && newItem is Group) { typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries + && oldItem.notes == newItem.notes } return typeContentTheSame && oldItem.nodeId == newItem.nodeId @@ -327,8 +354,8 @@ class NodeAdapter (private val context: Context, val iconColor = if (holder.container.isSelected) mContentSelectionColor else when (subNode.type) { - Type.GROUP -> mIconGroupColor - Type.ENTRY -> mIconEntryColor + Type.GROUP -> mTextColorPrimary + Type.ENTRY -> mTextColor } holder.imageIdentifier?.setColorFilter(iconColor) holder.icon.apply { @@ -348,14 +375,24 @@ class NodeAdapter (private val context: Context, } // Add meta text to show UUID holder.meta.apply { - if (mShowUUID) { - text = subNode.nodeId.toString() + val nodeId = subNode.nodeId?.toVisualString() + if (mShowUUID && nodeId != null) { + text = nodeId setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier) visibility = View.VISIBLE } else { visibility = View.GONE } } + // Add path to virtual group + if (mVirtualGroup) { + holder.path?.apply { + text = subNode.getPathString() + visibility = View.VISIBLE + } + } else { + holder.path?.visibility = View.GONE + } // Specific elements for entry if (subNode.type == Type.ENTRY) { @@ -398,6 +435,50 @@ class NodeAdapter (private val context: Context, holder.attachmentIcon?.visibility = if (entry.containsAttachment()) View.VISIBLE else View.GONE + // Assign colors + val backgroundColor = entry.backgroundColor + if (!holder.container.isSelected) { + if (backgroundColor != null) { + holder.container.setBackgroundColor(backgroundColor) + } else { + holder.container.setBackgroundColor(Color.TRANSPARENT) + } + } else { + holder.container.setBackgroundColor(mColorAccentLight) + } + val foregroundColor = entry.foregroundColor + if (!holder.container.isSelected) { + if (foregroundColor != null) { + holder.text.setTextColor(foregroundColor) + holder.subText?.setTextColor(foregroundColor) + holder.otpToken?.setTextColor(foregroundColor) + holder.otpProgress?.setIndicatorColor(foregroundColor) + holder.attachmentIcon?.setColorFilter(foregroundColor) + holder.meta.setTextColor(foregroundColor) + holder.icon.apply { + database.iconDrawableFactory.assignDatabaseIcon( + this, + subNode.icon, + foregroundColor + ) + } + } else { + holder.text.setTextColor(mTextColor) + holder.subText?.setTextColor(mTextColorSecondary) + holder.otpToken?.setTextColor(mTextColorSecondary) + holder.otpProgress?.setIndicatorColor(mTextColorSecondary) + holder.attachmentIcon?.setColorFilter(mTextColorSecondary) + holder.meta.setTextColor(mTextColor) + } + } else { + holder.text.setTextColor(mTextColorSelected) + holder.subText?.setTextColor(mTextColorSelected) + holder.otpToken?.setTextColor(mTextColorSelected) + holder.otpProgress?.setIndicatorColor(mTextColorSelected) + holder.attachmentIcon?.setColorFilter(mTextColorSelected) + holder.meta.setTextColor(mTextColorSelected) + } + database.stopManageEntry(entry) } @@ -430,15 +511,16 @@ class NodeAdapter (private val context: Context, OtpType.HOTP -> { holder?.otpProgress?.apply { max = 100 - progress = 100 + setProgressCompat(100, true) } } OtpType.TOTP -> { holder?.otpProgress?.apply { max = otpElement.period - progress = otpElement.secondsRemaining + setProgressCompat(otpElement.secondsRemaining, true) } } + null -> {} } holder?.otpToken?.apply { text = otpElement?.token @@ -497,8 +579,9 @@ class NodeAdapter (private val context: Context, var text: TextView = itemView.findViewById(R.id.node_text) var subText: TextView? = itemView.findViewById(R.id.node_subtext) var meta: TextView = itemView.findViewById(R.id.node_meta) + var path: TextView? = itemView.findViewById(R.id.node_path) var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) - var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) + var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress) var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token) var otpRunnable: OtpRunnable = OtpRunnable(otpContainer) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) @@ -506,6 +589,6 @@ class NodeAdapter (private val context: Context, } companion object { - private val TAG = NodeAdapter::class.java.name + private val TAG = NodesAdapter::class.java.name } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt index f5d121d73..88cdb8862 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt @@ -21,27 +21,29 @@ package com.kunzisoft.keepass.adapters import android.content.Context import android.database.Cursor +import android.database.MatrixCursor import android.graphics.Color +import android.provider.BaseColumns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.cursoradapter.widget.CursorAdapter import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.cursor.EntryCursorKDB -import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group -import com.kunzisoft.keepass.database.element.database.DatabaseKDB -import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.database.element.node.NodeId +import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.strikeOut +import java.util.* class SearchEntryCursorAdapter(private val context: Context, private val database: Database) - : androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) { + : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) { private val cursorInflater: LayoutInflater? = context.getSystemService( Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? @@ -70,6 +72,7 @@ class SearchEntryCursorAdapter(private val context: Context, viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon) viewHolder.textViewTitle = view.findViewById(R.id.entry_text) viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext) + viewHolder.textViewPath = view.findViewById(R.id.entry_path) view.tag = viewHolder return view @@ -101,32 +104,16 @@ class SearchEntryCursorAdapter(private val context: Context, visibility = if (text.isEmpty()) View.GONE else View.VISIBLE strikeOut(currentEntry.isCurrentlyExpires) } + + viewHolder.textViewPath?.apply { + text = currentEntry.getPathString() + } } } private fun getEntryFrom(cursor: Cursor): Entry? { - return database.createEntry()?.apply { - entryKDB?.let { entryKDB -> - (cursor as EntryCursorKDB).populateEntry(entryKDB, - { standardIconId -> - database.getStandardIcon(standardIconId) - }, - { customIconId -> - database.getCustomIcon(customIconId) - } - ) - } - entryKDBX?.let { entryKDBX -> - (cursor as EntryCursorKDBX).populateEntry(entryKDBX, - { standardIconId -> - database.getStandardIcon(standardIconId) - }, - { customIconId -> - database.getCustomIcon(customIconId) - } - ) - } - } + val entryCursor = cursor as EntryCursor + return database.getEntryById(entryCursor.getNodeId()) } override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { @@ -134,14 +121,7 @@ class SearchEntryCursorAdapter(private val context: Context, } private fun searchEntries(context: Context, query: String): Cursor? { - var cursorKDB: EntryCursorKDB? = null - var cursorKDBX: EntryCursorKDBX? = null - - if (database.type == DatabaseKDB.TYPE) - cursorKDB = EntryCursorKDB() - if (database.type == DatabaseKDBX.TYPE) - cursorKDBX = EntryCursorKDBX() - + val cursor = EntryCursor() val searchGroup = database.createVirtualGroupFromSearch(query, mOmitBackup, SearchHelper.MAX_SEARCH_ENTRY) @@ -149,17 +129,11 @@ class SearchEntryCursorAdapter(private val context: Context, // Search in hide entries but not meta-stream for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) { database.startManageEntry(entry) - entry.entryKDB?.let { - cursorKDB?.addEntry(it) - } - entry.entryKDBX?.let { - cursorKDBX?.addEntry(it) - } + cursor.addEntry(entry) database.stopManageEntry(entry) } } - - return cursorKDB ?: cursorKDBX + return cursor } fun getEntryFromPosition(position: Int): Entry? { @@ -176,5 +150,37 @@ class SearchEntryCursorAdapter(private val context: Context, var imageViewIcon: ImageView? = null var textViewTitle: TextView? = null var textViewSubTitle: TextView? = null + var textViewPath: TextView? = null + } + + private class EntryCursor : MatrixCursor(arrayOf( + ID, + COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS, + COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS + )) { + + private var entryId: Long = 0 + + fun addEntry(entry: Entry) { + addRow(arrayOf( + entryId, + entry.nodeId.id.mostSignificantBits, + entry.nodeId.id.leastSignificantBits + )) + entryId++ + } + + fun getNodeId(): NodeId { + return NodeIdUUID( + UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)), + getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))) + ) + } + + companion object { + const val ID = BaseColumns._ID + const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits" + const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits" + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt index 53698f3a8..8745b2bba 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt @@ -9,23 +9,23 @@ import android.widget.BaseAdapter import android.widget.ImageView import android.widget.TextView import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.icons.IconDrawableFactory -class TemplatesSelectorAdapter(private val context: Context, - private val iconDrawableFactory: IconDrawableFactory?, - private var templates: List