diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b13bae3bd..88566a8a7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -64,7 +64,7 @@ jobs: - uses: actions/checkout@v4 - uses: musichin/ktlint-check@v3 with: - ktlint-version: "1.1.1" + ktlint-version: "1.2.1" dependency-submission: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index be22f75e0..c3fc529a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Fixed a bug where invalid file paths were returned and retries did not work * Update to K2 (aka Kotlin `2.0.0`) * Update Compose BOM to 2024.05.00 * Update AndroidX to 1.13.1 diff --git a/lib/src/main/java/com/smileidentity/util/FileUtils.kt b/lib/src/main/java/com/smileidentity/util/FileUtils.kt index 8053b2760..6d43da080 100644 --- a/lib/src/main/java/com/smileidentity/util/FileUtils.kt +++ b/lib/src/main/java/com/smileidentity/util/FileUtils.kt @@ -1,6 +1,7 @@ package com.smileidentity.util import com.smileidentity.SmileID +import com.smileidentity.SmileIDCrashReporting import com.smileidentity.models.AuthenticationRequest import com.smileidentity.models.PrepUploadRequest import com.smileidentity.models.UploadRequest @@ -44,9 +45,7 @@ enum class FileType(val fileType: String) { DOCUMENT_BACK("si_document_back_"), ; - override fun toString(): String { - return fileType - } + override fun toString() = fileType } /** @@ -59,9 +58,10 @@ enum class FileType(val fileType: String) { * jobs for deletion. This enables clearing out space or managing files that are no longer needed * after job completion. Defaults to false to prevent accidental deletion of completed jobs. * - * @param deleteUnsubmittedJobs When set to true, the function targets files associated with unsubmitted - * jobs for deletion. Useful for resetting or clearing jobs that have not been completed or are - * no longer needed. Defaults to false to protect ongoing or queued jobs from unintended deletion. + * @param deleteUnsubmittedJobs When set to true, the function targets files associated with + * unsubmitted jobs for deletion. Useful for resetting or clearing jobs that have not been completed + * or are no longer needed. Defaults to false to protect ongoing or queued jobs from unintended + * deletion. * * @param jobIds An optional list of specific job IDs to delete. If provided, the cleanup process * is limited to these IDs, within the context of the completion status flags set. If null, the @@ -69,8 +69,9 @@ enum class FileType(val fileType: String) { * bulk cleanup operations. * * This function directly manipulates the file system by removing files and directories associated - * with the targeted jobs. It's designed to facilitate efficient storage management and job lifecycle - * maintenance within the system, ensuring that resources are allocated and used effectively. + * with the targeted jobs. It's designed to facilitate efficient storage management and job + * lifecycle maintenance within the system, ensuring that resources are allocated and used + * effectively. */ internal fun cleanupJobs( deleteSubmittedJobs: Boolean = false, @@ -103,20 +104,24 @@ internal fun cleanupJobs( } /** - * Initiates the cleanup process for jobs based on their scope and specified job IDs. This function allows for - * targeted cleanup operations, making it possible to selectively delete job-related files either from completed jobs, - * pending jobs, or both, depending on the scope provided. If job IDs are specified, the cleanup is restricted to those - * specific jobs; otherwise, it applies to all jobs within the specified scope. + * Initiates the cleanup process for jobs based on their scope and specified job IDs. This function + * allows for targeted cleanup operations, making it possible to selectively delete job-related + * files either from completed jobs, pending jobs, or both, depending on the scope provided. If job + * IDs are specified, the cleanup is restricted to those specific jobs; otherwise, it applies to all + * jobs within the specified scope. * - * @param scope The scope of the cleanup operation, determined by the DeleteScope enum. The default is DeleteScope.All, - * indicating that, by default, all jobs are targeted for cleanup. This parameter can be adjusted to target - * pending jobs or both pending and completed jobs, offering flexibility in how cleanup operations are conducted. - * @param jobIds An optional list of job IDs to specifically target for cleanup. If provided, only the files associated - * with these job IDs will be cleaned up, within the bounds of the specified scope. If null or not provided, the cleanup - * operation targets all jobs within the specified scope, allowing for bulk cleanup operations. + * @param scope The scope of the cleanup operation, determined by the DeleteScope enum. The default + * is DeleteScope.All, indicating that, by default, all jobs are targeted for cleanup. This + * parameter can be adjusted to target pending jobs or both pending and completed jobs, offering + * flexibility in how cleanup operations are conducted. + * @param jobIds An optional list of job IDs to specifically target for cleanup. If provided, only + * the files associated with these job IDs will be cleaned up, within the bounds of the specified + * scope. If null or not provided, the cleanup operation targets all jobs within the specified + * scope, allowing for bulk cleanup operations. * - * This function does not return any value, but it directly affects the file system by deleting files associated with - * the specified jobs. It's designed for internal use within the system to maintain cleanliness and manage storage efficiently. + * This function does not return any value, but it directly affects the file system by deleting + * files associated with the specified jobs. It's designed for internal use within the system to + * maintain cleanliness and manage storage efficiently. */ internal fun cleanupJobs(scope: DeleteScope = DeleteScope.All, jobIds: List? = null) { @@ -167,18 +172,21 @@ internal fun doGetSubmittedJobs(): List { /** * Lists the job IDs based on their completion status. This function can retrieve job IDs from both - * completed and pending categories, depending on the parameters provided. It allows for flexible retrieval, - * making it suitable for scenarios where either one or both types of job statuses are of interest. + * completed and pending categories, depending on the parameters provided. It allows for flexible + * retrieval, making it suitable for scenarios where either one or both types of job statuses are of + * interest. * * @param includeSubmitted A boolean flag that, when set to true, includes the IDs of completed jobs - * in the returned list. Defaults to true to ensure completed jobs are included unless explicitly excluded. + * in the returned list. Defaults to true to ensure completed jobs are included unless explicitly + * excluded. * @param includeUnsubmitted A boolean flag that, when set to true, includes the IDs of pending jobs - * in the returned list. Defaults to false, focusing the function on completed jobs unless pending jobs - * are explicitly requested. + * in the returned list. Defaults to false, focusing the function on completed jobs unless pending + * jobs are explicitly requested. * - * @return A list of strings representing the job IDs. The list may include IDs from either the completed - * or pending categories, or both, based on the flags provided. The order of IDs in the list is determined - * by the file system's enumeration order and is not guaranteed to follow any specific sorting. + * @return A list of strings representing the job IDs. The list may include IDs from either the + * completed or pending categories, or both, based on the flags provided. The order of IDs in the + * list is determined by the file system's enumeration order and is not guaranteed to follow any + * specific sorting. */ private fun listJobIds( includeSubmitted: Boolean = true, @@ -203,21 +211,26 @@ private fun listJobIds( } /** - * Retrieves a file of a specified type from a given folder, either from the submitted or unsubmitted directory. - * This function filters files based on their prefix which indicates the file type (e.g., "si_selfie_" for selfies), - * allowing for selective retrieval based on the file's purpose or content. + * Retrieves a file of a specified type from a given folder, either from the submitted or + * unsubmitted directory. This function filters files based on their prefix which indicates the file + * type (e.g., "si_selfie_" for selfies), allowing for selective retrieval based on the file's + * purpose or content. * - * @param folderName The name of the subfolder within the base save path from which to retrieve files. This allows for - * organization of files by job or category within the broader submitted or unsubmitted directories. - * @param fileType The type of file to retrieve, specified by the FileType enum. This parameter determines the prefix - * used to filter files in the directory (SELFIE, LIVENESS, DOCUMENT). - * @param savePath The base path where files are stored. Defaults to SmileID.fileSavePath, but can be overridden to - * target different storage locations. Useful for accessing files in environments with multiple storage directories. - * @param submitted A boolean flag indicating whether to retrieve files from the submitted (true) or unsubmitted (false) - * directory. This allows the function to adapt based on the processing stage of the files. + * @param folderName The name of the subfolder within the base save path from which to retrieve + * files. This allows for organization of files by job or category within the broader submitted or + * unsubmitted directories. + * @param fileType The type of file to retrieve, specified by the FileType enum. This parameter + * determines the prefix used to filter files in the directory (SELFIE, LIVENESS, DOCUMENT_FRONT, + * DOCUMENT_BACK). + * @param savePath The base path where files are stored. Defaults to SmileID.fileSavePath, but can + * be overridden to target different storage locations. Useful for accessing files in environments + * with multiple storage directories. + * @param submitted A boolean flag indicating whether to retrieve files from the submitted (true) or + * unsubmitted (false) directory. This allows the function to adapt based on the processing stage of + * the files. * - * @return A File object that matches the specified type and submission status within the specified folder. - * The file is filtered and sorted by name to ensure consistent ordering. + * @return A File object that matches the specified type and submission status within the specified + * folder. The file is filtered and sorted by name to ensure consistent ordering. */ fun getFileByType( folderName: String, @@ -234,21 +247,25 @@ fun getFileByType( } /** - * Retrieves a list of files of a specified type from a given folder, either from submitted or unsubmitted directories. - * This function filters files based on their prefix which indicates the file type (e.g., "si_selfie_" for selfies), - * allowing for selective retrieval based on the file's purpose or content. + * Retrieves a list of files of a specified type from a given folder, either from submitted or + * unsubmitted directories. This function filters files based on their prefix which indicates the + * file type (e.g., "si_selfie_" for selfies), allowing for selective retrieval based on the file's + * purpose or content. * - * @param folderName The name of the subfolder within the base save path from which to retrieve files. This allows for - * organization of files by job or category within the broader submitted or unsubmitted directories. - * @param fileType The type of files to retrieve, specified by the FileType enum. This parameter determines the prefix - * used to filter files in the directory (SELFIE, LIVENESS, DOCUMENT). - * @param savePath The base path where files are stored. Defaults to SmileID.fileSavePath, but can be overridden to - * target different storage locations. Useful for accessing files in environments with multiple storage directories. - * @param submitted A boolean flag indicating whether to retrieve files from the submitted (true) or unsubmitted (false) - * directory. This allows the function to adapt based on the processing stage of the files. + * @param folderName The name of the subfolder within the base save path from which to retrieve + * files. This allows for organization of files by job or category within the broader submitted or + * unsubmitted directories. + * @param fileType The type of files to retrieve, specified by the FileType enum. This parameter + * determines the prefix used to filter files in the directory (Selfie, Liveness, DOCUMENT). + * @param savePath The base path where files are stored. Defaults to SmileID.fileSavePath, but can + * be overridden to target different storage locations. Useful for accessing files in environments + * with multiple storage directories. + * @param submitted A boolean flag indicating whether to retrieve files from the submitted (true) or + * unsubmitted (false) directory. This allows the function to adapt based on the processing stage of + * the files. * - * @return A list of File objects that match the specified type and submission status within the specified folder. - * The files are filtered and sorted by name to ensure consistent ordering. + * @return A list of File objects that match the specified type and submission status within the + * specified folder. The files are filtered and sorted by name to ensure consistent ordering. */ fun getFilesByType( folderName: String, @@ -294,13 +311,17 @@ internal fun createSmileTempFile( /** * Constructs a `File` object for a temporary file, ensuring the path and file name adhere to - * expected formats and conditions. This method attempts to address potential edge cases - * related to file path construction and directory accessibility. + * expected formats and conditions. This method attempts to address potential edge cases related to + * file path construction and directory accessibility. * - * @param folderName The name of the folder where the file is saved. Must not be empty and should be a valid folder name. - * @param fileName The base name of the file. Must not be empty and should be a valid file name without special characters. - * @param state Indicates the state directory where the file is stored. True for UNSUBMITTED_PATH, false for SUBMITTED_PATH. - * @param savePath The root directory where the file is saved. Defaults to SmileID.fileSavePath. Must be accessible. + * @param folderName The name of the folder where the file is saved. Must not be empty and should be + * a valid folder name. + * @param fileName The base name of the file. Must not be empty and should be a valid file name + * without special characters. + * @param isUnsubmitted Indicates the isUnsubmitted directory where the file is stored. True for + * UNSUBMITTED_PATH, false for SUBMITTED_PATH. + * @param savePath The root directory where the file is saved. Defaults to SmileID.fileSavePath. + * Must be accessible. * @return The `File` object representing the exact file. * @throws IllegalArgumentException If any input parameters are invalid. * @throws IOException If the directory cannot be created or is not writable. @@ -308,7 +329,7 @@ internal fun createSmileTempFile( internal fun getSmileTempFile( folderName: String, fileName: String, - state: Boolean = true, + isUnsubmitted: Boolean = true, savePath: String = SmileID.fileSavePath, ): File { if (folderName.isBlank() || fileName.isBlank()) { @@ -317,7 +338,7 @@ internal fun getSmileTempFile( ) } - val stateDirectory = if (state) UNSUBMITTED_PATH else SUBMITTED_PATH + val stateDirectory = if (isUnsubmitted) UNSUBMITTED_PATH else SUBMITTED_PATH val directory = File(savePath, "$stateDirectory/$folderName") if (!directory.exists() && !directory.mkdirs()) { @@ -381,7 +402,11 @@ internal fun createSmileJsonFile(fileName: String, folderName: String): File { /** * Moves a folder from 'unsubmitted' to 'submitted' within the app's specific directory, handling - * all edge cases. + * all edge cases. We copy files recursively as opposed to moving the entire folder so that we can + * merge contents (i.e. in the case something got moved to submitted but was later retried - this + * can happen in the case when Offline Mode is disabled, but there was an error and the user + * retries). + * * @param folderName The name of the job or operation, corresponding to the folder to be moved. * @param savePath The base path where the 'pending' and 'complete' folders are * located, defaulting to SmileID.fileSavePath. @@ -395,7 +420,9 @@ internal fun moveJobToSubmitted( val submittedPath = File(savePath, "$SUBMITTED_PATH/$folderName") if (!unSubmittedPath.exists() || !unSubmittedPath.isDirectory) { - println("Source directory does not exist or is not a directory") + val message = "Unsubmitted directory does not exist or is not a directory" + Timber.v(message) + SmileIDCrashReporting.hub.addBreadcrumb(message) return false } diff --git a/lib/src/main/java/com/smileidentity/util/Util.kt b/lib/src/main/java/com/smileidentity/util/Util.kt index fad6b2600..a7c9e37c0 100644 --- a/lib/src/main/java/com/smileidentity/util/Util.kt +++ b/lib/src/main/java/com/smileidentity/util/Util.kt @@ -259,17 +259,25 @@ fun getExceptionHandler(proxy: (Throwable) -> Unit) = CoroutineExceptionHandler proxy(converted) } +/** + * Handles file moving in a failure scenario. If offline mode *is not* enabled, the job is moved to + * the submitted directory. If offline mode *is* enabled, the job is moved to submitted only if the + * error is not a network error. Otherwise, (if Offline Mode is enabled, and it is a network error), + * the job is left in the unsubmitted directory (either to be retried or submitted later). + * + * @return if the job was moved to the submitted directory + */ fun handleOfflineJobFailure( jobId: String, throwable: Throwable, exceptionHandler: ( (Throwable) -> Unit )? = null, -) { - Timber.e(throwable, "Error in submitJob for jobId: $jobId") +): Boolean { + var didMove = false if (!(SmileID.allowOfflineMode && isNetworkFailure(throwable))) { - val complete = moveJobToSubmitted(jobId) - if (!complete) { + didMove = moveJobToSubmitted(jobId) + if (!didMove) { Timber.w("Failed to move job $jobId to complete") SmileIDCrashReporting.hub.addBreadcrumb( Breadcrumb().apply { @@ -281,6 +289,7 @@ fun handleOfflineJobFailure( } } exceptionHandler?.let { it(throwable) } + return didMove } fun randomId(prefix: String) = prefix + "-" + java.util.UUID.randomUUID().toString() diff --git a/lib/src/main/java/com/smileidentity/viewmodel/BiometricKycViewModel.kt b/lib/src/main/java/com/smileidentity/viewmodel/BiometricKycViewModel.kt index 945126451..b931c3865 100644 --- a/lib/src/main/java/com/smileidentity/viewmodel/BiometricKycViewModel.kt +++ b/lib/src/main/java/com/smileidentity/viewmodel/BiometricKycViewModel.kt @@ -67,8 +67,11 @@ class BiometricKycViewModel( private fun submitJob(selfieFile: File, livenessFiles: List) { _uiState.update { it.copy(processingState = ProcessingState.InProgress) } val proxy = fun(e: Throwable) { - Timber.e(e) - handleOfflineJobFailure(jobId, e) + val didMoveToSubmitted = handleOfflineJobFailure(jobId, e) + if (didMoveToSubmitted) { + this.selfieFile = getFileByType(jobId, FileType.SELFIE) + this.livenessFiles = getFilesByType(jobId, FileType.LIVENESS) + } if (SmileID.allowOfflineMode && isNetworkFailure(e)) { result = SmileIDResult.Success( BiometricKycResult( diff --git a/lib/src/main/java/com/smileidentity/viewmodel/SelfieViewModel.kt b/lib/src/main/java/com/smileidentity/viewmodel/SelfieViewModel.kt index 50490833b..4e5e4a122 100644 --- a/lib/src/main/java/com/smileidentity/viewmodel/SelfieViewModel.kt +++ b/lib/src/main/java/com/smileidentity/viewmodel/SelfieViewModel.kt @@ -262,7 +262,14 @@ class SelfieViewModel( _uiState.update { it.copy(processingState = ProcessingState.InProgress) } val proxy = fun(e: Throwable) { - handleOfflineJobFailure(jobId, e) + val didMoveToSubmitted = handleOfflineJobFailure(jobId, e) + if (didMoveToSubmitted) { + this.selfieFile = getFileByType(jobId, FileType.SELFIE) + this.livenessFiles.apply { + clear() + addAll(getFilesByType(jobId, FileType.LIVENESS)) + } + } if (SmileID.allowOfflineMode && isNetworkFailure(e)) { result = SmileIDResult.Success( SmartSelfieResult( diff --git a/lib/src/main/java/com/smileidentity/viewmodel/document/OrchestratedDocumentViewModel.kt b/lib/src/main/java/com/smileidentity/viewmodel/document/OrchestratedDocumentViewModel.kt index fb55000c3..6ae3b64f9 100644 --- a/lib/src/main/java/com/smileidentity/viewmodel/document/OrchestratedDocumentViewModel.kt +++ b/lib/src/main/java/com/smileidentity/viewmodel/document/OrchestratedDocumentViewModel.kt @@ -246,7 +246,11 @@ internal abstract class OrchestratedDocumentViewModel( * Trigger the display of the Error dialog */ fun onError(throwable: Throwable) { - handleOfflineJobFailure(jobId, throwable) + val didMoveToSubmitted = handleOfflineJobFailure(jobId, throwable) + if (didMoveToSubmitted) { + this.selfieFile = getFileByType(jobId, FileType.SELFIE) + this.livenessFiles = getFilesByType(jobId, FileType.LIVENESS) + } stepToRetry = uiState.value.currentStep _uiState.update { it.copy(