Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Make a copy and Select" option into built-in File Picker #293

Merged

Conversation

aivanovski
Copy link
Owner

@aivanovski aivanovski commented Jan 31, 2025

Summary by CodeRabbit

Based on the comprehensive changes, here are the release notes:

  • New Features

    • Added a new option dialog for file and storage interactions
    • Introduced enhanced file picker functionality with context menu
    • Implemented more robust file handling and error management
  • Improvements

    • Streamlined file system operations with better error handling
    • Enhanced biometric unlock process
    • Improved dialog and cell view model structures
  • Bug Fixes

    • Updated file copying mechanisms
    • Refined key file and password decoding processes
  • Refactoring

    • Renamed several view models and cell models for consistency
    • Simplified event handling in multiple components
  • UI Changes

    • Added new dialog and cell layouts
    • Introduced long-click support for file cells

These release notes provide a high-level overview of the significant changes while maintaining confidentiality about internal implementation details.

Copy link

coderabbitai bot commented Jan 31, 2025

Walkthrough

The pull request introduces a comprehensive enhancement to the file handling and user interface components of the application. Key changes include the implementation of a new option dialog system, improvements to file system operations, and refinements to view models and cell models. The modifications focus on providing more robust file management capabilities, introducing long-click support for file items, and creating a more flexible dialog mechanism for presenting user options.

Changes

File Change Summary
RegularFileSystemProvider.kt Added file existence check method and enhanced error handling for file write operations
FilePickerInteractor.kt Added methods for copying files to private storage and opening files
UiModule.kt Added dependency injection for option dialog components
OptionDialog.kt, OptionDialogArgs.kt, OptionDialogViewModel.kt Implemented new option dialog system with flexible configuration
FilePickerViewModel.kt Enhanced file selection and menu handling with new long-click and copy options
InputOutputUtils.kt Added new copy method with improved stream handling
Layout files Updated and added new layouts for one-line and two-line cell views

Poem

🐰 A Rabbit's Ode to Code Refinement

With whiskers twitching and code so bright,
Files dance through dialogs, a programmer's delight!
Long-clicks and options, now smooth as can be,
Refactored with love, from tail to key.

Hop, hop, hooray for cleaner design! 🚀


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@aivanovski
Copy link
Owner Author

@coderabbitai review

Copy link

coderabbitai bot commented Jan 31, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (16)
app/src/main/res/layout/cell_option_one_line.xml (2)

6-11: Consider adding documentation for the ViewModel binding.

While the binding is correctly implemented, adding a comment describing the purpose and usage of OneLineTextCellViewModel would improve maintainability.

 <data>
+    <!-- OneLineTextCellViewModel handles the presentation logic for single-line option items
+         in the option dialog, managing text display and click events. -->
     <variable
         name="viewModel"
         type="com.ivanovsky.passnotes.presentation.core.viewmodel.OneLineTextCellViewModel" />
 </data>

13-26: Enhance accessibility and RTL support.

The TextView implementation is good, but consider these improvements for better accessibility and internationalization:

 <TextView
     style="@style/Material.ListItem.PrimaryText.SingleLine"
     android:layout_width="match_parent"
     android:layout_height="@dimen/material_list_item_height_single_line_with_icon"
     android:background="?android:attr/selectableItemBackground"
     android:clickable="true"
     android:focusable="true"
     android:gravity="center_vertical"
     android:onClick="@{() -> viewModel.onClicked()}"
-    android:paddingLeft="@dimen/material_list_item_horizontal_padding"
-    android:paddingRight="@dimen/material_list_item_horizontal_padding"
+    android:paddingStart="@dimen/material_list_item_horizontal_padding"
+    android:paddingEnd="@dimen/material_list_item_horizontal_padding"
     android:text="@{viewModel.model.text}"
     android:textStyle="bold"
+    android:contentDescription="@{viewModel.model.text}"
     tools:text="Primary text" />
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt (1)

384-410: Improve error handling and add clarity to edge cases.

While the refactoring improves the code structure, consider these enhancements:

  1. Add a comment explaining the early return condition
  2. Provide more context in the IllegalStateException
  3. Consider handling errors more gracefully instead of throwing exceptions
 val keyFile = selectedKeyFile

-if (selectedUsedFile.keyType == KeyType.KEY_FILE && keyFile == null) return
+// Early return if key file is required but not provided
+if (selectedUsedFile.keyType == KeyType.KEY_FILE && keyFile == null) {
+    setErrorPanelState(OperationError.newError("Key file is required but not provided"))
+    return
+}

 setScreenState(ScreenState.loading())

 viewModelScope.launch {
     val decodePasswordResult = interactor.decodePassword(decoder, biometricData)
     if (decodePasswordResult.isFailed) {
         setErrorPanelState(decodePasswordResult.error)
         return@launch
     }

     val password = decodePasswordResult.getOrThrow()
     val keyType = selectedUsedFile.keyType

     val key = when {
         keyType == KeyType.PASSWORD -> PasswordKeepassKey(password)

         keyType == KeyType.KEY_FILE && keyFile != null -> FileKeepassKey(
             file = keyFile,
             password = password
         )

-        else -> throw IllegalStateException()
+        else -> throw IllegalStateException("Invalid key type configuration: keyType=$keyType, keyFile=${keyFile != null}")
     }
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/TwoLineTextCellViewModel.kt (1)

17-20: Consider using string template for event constant.

The event constant could be more readable using string template syntax.

-        val CLICK_EVENT = TwoLineTextCellViewModel::class.qualifiedName + "_clickEvent"
+        val CLICK_EVENT = "${TwoLineTextCellViewModel::class.qualifiedName}_clickEvent"
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellModelFactory.kt (1)

24-35: Consider using a more robust ID generation strategy.

Using the index as a string ID might lead to conflicts if the list is modified or if IDs are used across different lists. Consider using a more unique identifier generation strategy.

Apply this diff to improve ID generation:

-                    id = index.toString(),
+                    id = "${item.hashCode()}_$index",

This change makes the IDs more unique by combining the item's hash code with the index.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogViewModel.kt (1)

63-73: Consider dependency injection alternatives to GlobalInjector.

Using GlobalInjector directly in the Factory class could make testing more difficult and creates a tight coupling. Consider using constructor injection or a provider pattern instead.

Apply this diff to improve dependency injection:

 class Factory(
-    private val args: OptionDialogArgs
+    private val args: OptionDialogArgs,
+    private val modelFactory: OptionDialogCellModelFactory,
+    private val viewModelFactory: OptionDialogCellViewModelFactory
 ) : ViewModelProvider.Factory {

     @Suppress("UNCHECKED_CAST")
     override fun <T : ViewModel> create(modelClass: Class<T>): T {
-        return GlobalInjector.get<OptionDialogViewModel>(
-            parametersOf(args)
-        ) as T
+        return OptionDialogViewModel(
+            modelFactory,
+            viewModelFactory,
+            args
+        ) as T
     }
 }
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialog.kt (2)

37-37: Consider using a weak reference or clearing the callback.

The onItemClick callback is stored directly in the fragment, which could potentially cause memory leaks if the dialog is dismissed but the callback reference is retained.

Consider using a weak reference or clearing the callback in onDestroy:

-    private var onItemClick: ((index: Int) -> Unit)? = null
+    private var onItemClick = WeakReference<((index: Int) -> Unit)?>(null)

+    override fun onDestroy() {
+        super.onDestroy()
+        onItemClick.clear()
+    }

89-106: Add KDoc documentation for the factory method.

Consider adding documentation to clarify the purpose and usage of the factory method.

Add documentation like this:

+    /**
+     * Creates a new instance of OptionDialog.
+     *
+     * @param args The arguments to configure the dialog
+     * @param onItemClick Callback invoked when an item is selected
+     * @return A new instance of OptionDialog
+     */
     fun newInstance(
         args: OptionDialogArgs,
         onItemClick: (index: Int) -> Unit
     ): OptionDialog =
app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt (1)

213-214: Consider grouping related dialog components.

The dialog-related factory definitions are separated. Consider grouping them together for better organization.

Move the option dialog factories near other dialog-related components:

-            single { OptionDialogCellModelFactory(get()) }
-            single { OptionDialogCellViewModelFactory(get()) }

             // ViewModels
+            // Dialog ViewModels
+            single { OptionDialogCellModelFactory(get()) }
+            single { OptionDialogCellViewModelFactory(get()) }
             factory { (args: OptionDialogArgs) ->
                 OptionDialogViewModel(
                     get(),
                     get(),
                     args
                 )
             }

Also applies to: 392-398

app/src/main/res/layout/core_base_cell_dialog.xml (1)

28-37: Add content description for accessibility.

The RecyclerView should have a content description for better accessibility support.

Add content description:

         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/recyclerView"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:contentDescription="@string/options_list"
             android:orientation="vertical"
             app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app/src/main/res/layout/cell_option_two_line.xml (1)

21-47: Enhance accessibility and verify RTL support.

The TextViews should have content descriptions for accessibility, and the layout should be verified for RTL support.

Add accessibility support and verify RTL:

         <TextView
             android:id="@+id/title"
             style="@style/Material.PrimaryText.SingleLine"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginHorizontal="@dimen/element_margin"
+            android:layout_marginStart="@dimen/element_margin"
+            android:layout_marginEnd="@dimen/element_margin"
+            android:importantForAccessibility="yes"
             android:text="@{viewModel.model.title}"
             android:textStyle="bold"
             tools:text="Title" />

         <TextView
             android:id="@+id/description"
             style="@style/Material.SecondaryText"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:importantForAccessibility="yes"
             android:text="@{viewModel.model.description}"
             tools:text="Description" />
app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt (1)

170-187: Verify parent directory existence before file creation.

While the file existence check is good, consider verifying that the parent directory exists and is writable before attempting to create the file.

 return lock.withLock {
     val isExistResult = exists(file)
     if (isExistResult.isFailed) {
         return@withLock isExistResult.takeError()
     }

     val isExist = isExistResult.getOrThrow()
     if (isExist && onConflictStrategy == OnConflictStrategy.CANCEL) {
         return@withLock OperationResult.error(newFileIsAlreadyExistsError())
     }

+    val parent = File(file.path).parentFile
+    if (parent != null && !parent.exists() && !parent.mkdirs()) {
+        return@withLock OperationResult.error(
+            newGenericIOError("Failed to create parent directory")
+        )
+    }
+
     try {
         val out = BufferedOutputStream(FileOutputStream(file.path))
         OperationResult.success(out)
     } catch (e: Exception) {
         Timber.d(e)
         OperationResult.error(newGenericIOError(e.message, e))
     }
 }
app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt (1)

81-90: Consider adding timeout for file operations.

While the implementation is correct, long-running file operations could potentially block the UI thread.

Consider adding a timeout parameter with a default value:

-private suspend fun openFile(file: FileDescriptor): OperationResult<InputStream> =
+private suspend fun openFile(
+    file: FileDescriptor,
+    timeoutMs: Long = 30000
+): OperationResult<InputStream> =
     withContext(dispatchers.IO) {
-        val fsProvider = fileSystemResolver.resolveProvider(file.fsAuthority)
+        withTimeout(timeoutMs) {
+            val fsProvider = fileSystemResolver.resolveProvider(file.fsAuthority)
 
-        fsProvider.openFileForRead(
-            file,
-            OnConflictStrategy.CANCEL,
-            FSOptions.READ_ONLY
-        )
+            fsProvider.openFileForRead(
+                file,
+                OnConflictStrategy.CANCEL,
+                FSOptions.READ_ONLY
+            )
+        }
     }
app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt (1)

133-144: Consider adding progress tracking for large files.

For large file operations, it would be helpful to track and report progress.

Consider adding a progress callback:

 fun copyOrThrow(
     from: InputStream,
     to: OutputStream,
     isCloseOnFinish: Boolean,
-    cancellation: AtomicBoolean
+    cancellation: AtomicBoolean,
+    onProgress: ((bytesTransferred: Long) -> Unit)? = null
 ) {
     try {
         val buf = ByteArray(BUFFER_SIZE)
         var len: Int
+        var totalBytesTransferred: Long = 0
         while (from.read(buf).also { len = it } > 0 && !cancellation.get()) {
             to.write(buf, 0, len)
+            totalBytesTransferred += len
+            onProgress?.invoke(totalBytesTransferred)
         }
         to.flush()
     } finally {
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt (1)

169-191: Consider handling dialog dismissal.

The dialog implementation should handle dismissal gracefully.

Add dialog dismissal handling:

 private fun showFileMenuDialog(items: List<FileMenuItem>) {
     val options = items.map { item ->
         when (item) {
             FileMenuItem.SELECT -> OptionItem(
                 title = resources.getString(R.string.select),
                 description = null
             )

             FileMenuItem.COPY_AND_SELECT -> OptionItem(
                 title = resources.getString(R.string.make_a_copy_and_select),
                 description = resources.getString(R.string.copy_and_select_description)
             )
         }
     }

     val dialog = OptionDialog.newInstance(
         args = OptionDialogArgs(options),
         onItemClick = { index ->
             viewModel.onFileMenuClicked(items[index])
-        }
+        },
+        onDismiss = {
+            viewModel.onFileMenuDismissed()
+        }
     )
     dialog.show(childFragmentManager, OptionDialog.TAG)
 }
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt (1)

153-167: Consider adding progress indication for file copy operations.

Long-running copy operations should show progress to improve user experience.

Add progress tracking:

 private fun copyAndSelectFile(file: FileDescriptor) {
     setScreenState(ScreenState.loading())
 
     viewModelScope.launch {
-        val copyResult = interactor.copyToPrivateStorage(file)
+        val copyResult = interactor.copyToPrivateStorage(
+            file = file,
+            onProgress = { bytesTransferred ->
+                setScreenState(
+                    ScreenState.loading(
+                        progress = bytesTransferred,
+                        total = file.size
+                    )
+                )
+            }
+        )
         if (copyResult.isFailed) {
             val message = errorInteractor.processAndGetMessage(copyResult.error)
             setScreenState(ScreenState.dataWithError(message))
             return@launch
         }

         val copiedFile = copyResult.getOrThrow()
         selectFile(copiedFile)
     }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b46ff7a and ccb5d84.

📒 Files selected for processing (29)
  • app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt (5 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt (2 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt (3 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialog.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogArgs.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogViewModel.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellModelFactory.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellViewModelFactory.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/model/OptionItem.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/OneLineTextCellModel.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/TwoLineTextCellModel.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/FileCellViewModel.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/OneLineTextCellViewModel.kt (2 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/TwoLineTextCellViewModel.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt (4 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt (8 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt (3 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/dialog/ChooseOptionDialog.kt (1 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/StorageListViewModel.kt (3 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellModelFactory.kt (2 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellViewModelFactory.kt (2 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt (4 hunks)
  • app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt (4 hunks)
  • app/src/main/res/layout/cell_file.xml (1 hunks)
  • app/src/main/res/layout/cell_option_one_line.xml (1 hunks)
  • app/src/main/res/layout/cell_option_two_line.xml (1 hunks)
  • app/src/main/res/layout/cell_single_text.xml (1 hunks)
  • app/src/main/res/layout/core_base_cell_dialog.xml (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/OneLineTextCellModel.kt
  • app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/StorageListViewModel.kt
🧰 Additional context used
🪛 detekt (1.23.7)
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt

[warning] 605-605: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions.

(detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause)

🔇 Additional comments (30)
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/OneLineTextCellViewModel.kt (2)

6-6: LGTM! Class and model type renamed consistently.

The renaming from SingleTextCellViewModel to OneLineTextCellViewModel and corresponding model type update is consistent with the refactoring effort.

Also applies to: 8-9


19-19: LGTM! Event constant updated correctly.

The event constant has been updated to use the new class name while maintaining the same pattern.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellViewModelFactory.kt (1)

7-7: LGTM! Factory updated consistently with renamed types.

The import statements and model type check in the factory have been updated correctly to use the new class names.

Also applies to: 9-9, 19-19

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellModelFactory.kt (1)

8-8: LGTM! Model factory updated consistently with renamed types.

The import statement and model instantiation have been updated correctly to use the new class name.

Also applies to: 34-34

app/src/main/res/layout/cell_single_text.xml (1)

10-10: LGTM! Layout data binding updated consistently.

The ViewModel type in the data binding has been updated correctly to use the new class name.

app/src/main/res/layout/cell_option_one_line.xml (1)

1-4: LGTM!

The XML declaration and layout structure follow Android data binding best practices.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt (3)

36-36: LGTM!

The addition of getOrThrow extension function import improves error handling in the biometric unlock flow.


200-201: LGTM!

The simplified biometric authentication check improves code readability while maintaining the same functionality.


899-900: LGTM!

The multi-line formatting of the condition improves code readability.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt (2)

25-25: LGTM!

The import is correctly added and necessary for handling the new key type.


1322-1322: Verify biometric unlock support for all key types.

The condition has been simplified to support biometric unlock for all key types, which aligns with the support for both password and file-based keys.

Run the following script to verify biometric unlock support:

✅ Verification successful

Biometric unlock properly supports all key types

The implementation correctly handles biometric unlock for both password-based and key file-based authentication methods. The biometric authentication process is key-type agnostic and properly integrates with both authentication flows after successful biometric verification.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that biometric unlock is properly handled for all key types.

# Test: Search for key type checks in biometric-related code.
rg -A 5 'biometric|KeyType' --type kotlin

Length of output: 81079

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/FileCellViewModel.kt (2)

17-19: LGTM! Clean implementation of long click handling.

The onLongClicked() method follows the same pattern as the existing onClicked() method, maintaining consistency in event handling.


21-24: LGTM! Consistent constant naming.

The LONG_CLICK_EVENT constant follows the same naming pattern as CLICK_EVENT, maintaining code consistency.

app/src/main/res/layout/cell_file.xml (1)

21-22: Verify the long click binding implementation.

The long click binding looks correct, but please ensure:

  1. The binding adapter for app:onLongClick is properly registered
  2. The long click handler doesn't interfere with the regular click handler
✅ Verification successful

Long click binding implementation is correct

The binding adapter for app:onLongClick is properly registered and implemented in BindingAdapters.kt. The implementation correctly handles event consumption and won't interfere with the regular click handler.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for binding adapter registration for onLongClick

# Search for binding adapter
ast-grep --pattern 'class $_ {
  $$$
  @BindingAdapter("onLongClick")
  fun $_(view: View, $handler: $type) {
    $$$
  }
  $$$
}'

# Search for potential conflicts in click handling
rg -A 5 'onLongClick.*onClicked'

Length of output: 167


Script:

#!/bin/bash
# Broader search for long click related code
# Search for binding adapters with different naming patterns
ast-grep --pattern '@BindingAdapter($pattern)
fun $_($$$) {
  $$$
}'

# Search for any long click related code
rg -i "longclick|long_click" -A 5

# Search for binding utility files
fd -e kt -e java "bind|binding" --exec cat {}

Length of output: 41520

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/TwoLineTextCellModel.kt (1)

3-7: LGTM! Well-structured data class implementation.

The data class follows Kotlin best practices with clear property definitions and appropriate inheritance from BaseCellModel.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/model/OptionItem.kt (1)

6-10: LGTM! Well-structured Parcelable data class implementation.

The data class follows Kotlin best practices with appropriate use of @Parcelize annotation and clear property definitions.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogArgs.kt (1)

7-10: LGTM! Well-structured Parcelable data class implementation.

The data class follows Kotlin best practices with appropriate use of @Parcelize annotation and clear property definition for the options list.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/TwoLineTextCellViewModel.kt (1)

8-15: LGTM! Well-structured ViewModel implementation.

The class follows MVVM pattern best practices with clear separation of concerns and proper event handling.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/dialog/ChooseOptionDialog.kt (1)

10-10: LGTM! Appropriate deprecation notice.

The deprecation notice clearly indicates the replacement class to use.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellViewModelFactory.kt (1)

15-28: LGTM! Well-structured factory implementation.

The factory correctly handles all cell model types with appropriate error handling for unsupported models.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogViewModel.kt (1)

34-46: LGTM! Clean event handling implementation.

The event subscription is well-structured with proper null safety checks and clear event handling logic.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialog.kt (1)

47-74: LGTM! Well-structured view setup.

The view setup follows Android best practices with proper theming and adapter configuration.

app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt (2)

15-15: LGTM!

The new import is correctly placed and necessary for the enhanced error handling.


111-115: Consider validating external storage root path.

While using firstOrNull() is safe, consider adding validation to ensure the selected external storage path is accessible and writable.

Run this script to check external storage paths:

app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt (1)

35-79: LGTM! Well-structured implementation of copyToPrivateStorage.

The implementation follows best practices with:

  • Proper error handling and mapping
  • Clear separation of concerns
  • Resource cleanup
app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt (1)

42-59: LGTM! Well-implemented stream copy operation.

The implementation includes:

  • Proper resource management
  • Comprehensive error handling
  • Thread-safe operation
app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt (1)

151-153: LGTM! Clean event observation implementation.

The event handling follows MVVM pattern correctly.

app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt (2)

132-139: LGTM! Clean implementation of file menu handling.

The menu handling follows MVVM pattern with proper null checks.


350-370: 🛠️ Refactor suggestion

Consider adding file size check before copy operation.

To prevent potential issues with large files, add a size check before copying.

Add size validation:

 private fun onItemLongClicked(filePath: String?) {
     val file = filePathToFileMap[filePath] ?: return
 
     selectedFile = file
 
     val menuItems = mutableListOf<FileMenuItem>()
 
     if ((args.action == Action.PICK_FILE && !file.isDirectory) ||
         (args.action == Action.PICK_DIRECTORY && file.isDirectory)
     ) {
         menuItems.add(FileMenuItem.SELECT)
     }
 
-    if (!file.isDirectory && file.fsAuthority.type != FSType.INTERNAL_STORAGE) {
+    if (!file.isDirectory && 
+        file.fsAuthority.type != FSType.INTERNAL_STORAGE &&
+        file.size <= MAX_COPY_FILE_SIZE
+    ) {
         menuItems.add(FileMenuItem.COPY_AND_SELECT)
     }
 
     if (menuItems.isNotEmpty()) {
         showFileMenuDialog.call(menuItems)
     }
 }

+private companion object {
+    const val MAX_COPY_FILE_SIZE = 100 * 1024 * 1024L // 100MB
+}
✅ Verification successful

Add size check to prevent internal storage issues

The suggestion to add a size check is valid. The code copies files to internal storage without any size limits, which could lead to storage space issues. The proposed 100MB limit is reasonable for internal storage operations.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if there are any size limits defined in the codebase

# Search for size-related constants
rg -i 'size|limit|max.*bytes' --type kotlin

Length of output: 17750


Script:

#!/bin/bash
# Find the implementation of copy operation
ast-grep --pattern 'fun $COPY_METHOD($$$) {
  $$$
}'

# Also search for any copy-related methods
rg -g '*.kt' -A 5 'copy.*select|COPY_AND_SELECT'

Length of output: 68760


Script:

#!/bin/bash
# Find COPY_AND_SELECT definition and usage
rg -g '*.kt' -B 2 -A 2 'enum.*FileMenuItem|COPY_AND_SELECT'

# Find where FileMenuItem is handled
ast-grep --pattern 'when ($expr) {
  $$$
  FileMenuItem.COPY_AND_SELECT -> $$$
  $$$
}'

Length of output: 2805


Script:

#!/bin/bash
# Find copyAndSelectFile implementation
ast-grep --pattern 'fun copyAndSelectFile($$$) {
  $$$
}'

# Also search for any copy utilities
rg -g '*.kt' -B 2 -A 10 'fun.*copy.*file|class.*copy.*file'

Length of output: 3510


Script:

#!/bin/bash
# Get the complete implementation of copyToPrivateStorage
rg -g '*.kt' -B 5 -A 30 'fun copyToPrivateStorage'

# Find any file copy utilities
rg -g '*.kt' 'fun.*copy.*file|class.*copy.*file|BUFFER_SIZE|copy.*stream'

Length of output: 5472

app/src/main/res/values/strings.xml (1)

293-294: LGTM! Well-structured string resources.

The new string resources are properly organized, follow naming conventions, and provide clear user-facing text that aligns with the PR's objective of adding a "Make a copy and Select" option to the built-in File Picker.

Comment on lines +602 to +606
val password = when (val key = getKeyResult.getOrThrow()) {
is PasswordKeepassKey -> key.password
is FileKeepassKey -> key.password
else -> throw IllegalStateException()
} ?: EMPTY
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add a descriptive message to IllegalStateException.

The type handling is good, but the exception should include a message explaining why it was thrown.

Apply this diff to improve error handling:

-                else -> throw IllegalStateException()
+                else -> throw IllegalStateException("Unsupported key type: ${key::class.simpleName}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val password = when (val key = getKeyResult.getOrThrow()) {
is PasswordKeepassKey -> key.password
is FileKeepassKey -> key.password
else -> throw IllegalStateException()
} ?: EMPTY
val password = when (val key = getKeyResult.getOrThrow()) {
is PasswordKeepassKey -> key.password
is FileKeepassKey -> key.password
else -> throw IllegalStateException("Unsupported key type: ${key::class.simpleName}")
} ?: EMPTY
🧰 Tools
🪛 detekt (1.23.7)

[warning] 605-605: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions.

(detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause)

@aivanovski aivanovski merged commit 1299f93 into master Jan 31, 2025
8 checks passed
@aivanovski aivanovski deleted the feature/add-option-to-copy-db-key-into-private-storage branch January 31, 2025 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant