From 8c1b855b055c079c57eda98c33af72bc43ed7849 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:44:56 +0000 Subject: [PATCH 01/12] refactor(scan): remove PDF-from-images background job Dead code once nothing calls startPdfGenerateAndUploadWork(): this job only existed to combine DocumentScanActivity's scanned pages into one PDF for upload, and that flow is being replaced by an embedded FairScan, which produces the finished PDF itself. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- .../documentscan/GeneratePDFUseCaseTest.kt | 71 ---------- .../client/documentscan/GeneratePDFUseCase.kt | 62 --------- .../documentscan/GeneratePdfFromImagesWork.kt | 122 ------------------ .../client/jobs/BackgroundJobFactory.kt | 15 --- .../client/jobs/BackgroundJobManager.kt | 2 - .../client/jobs/BackgroundJobManagerImpl.kt | 20 --- app/src/main/res/values/strings.xml | 2 - .../client/jobs/BackgroundJobFactoryTest.kt | 5 - 8 files changed, 299 deletions(-) delete mode 100644 app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt diff --git a/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt b/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt deleted file mode 100644 index 36402fb52d5f..000000000000 --- a/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.graphics.pdf.PdfRenderer -import android.os.ParcelFileDescriptor -import com.nextcloud.client.logger.Logger -import com.owncloud.android.AbstractIT -import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import java.io.File - -internal class GeneratePDFUseCaseTest : AbstractIT() { - - @MockK - private lateinit var logger: Logger - - private lateinit var sut: GeneratePDFUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - sut = GeneratePDFUseCase(logger) - } - - @Test - fun invalidArguments_shouldReturnFalse() { - var result = sut.execute(emptyList(), "/test/foo.pdf") - assertFalse("Usecase does not indicate failure with invalid arguments", result) - result = sut.execute(listOf("/test.jpg"), "") - assertFalse("Usecase does not indicate failure with invalid arguments", result) - } - - @Test - fun generatePdf_checkPages() { - // can't think of how to test the _content_ of the pages - val images = listOf( - getFile("image.jpg"), - getFile("christine.jpg") - ).map { it.path } - - val output = "/sdcard/test.pdf" - - val result = sut.execute(images, output) - - assertTrue("Usecase does not indicate success", result) - - val outputFile = File(output) - - assertTrue("Output file does not exist", outputFile.exists()) - - ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { - PdfRenderer(it).use { renderer -> - val pageCount = renderer.pageCount - assertTrue("Page count is not correct", pageCount == 2) - } - } - - // clean up - outputFile.delete() - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt deleted file mode 100644 index 87bd71b87e17..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.graphics.BitmapFactory -import android.graphics.pdf.PdfDocument -import com.nextcloud.client.logger.Logger -import java.io.FileOutputStream -import java.io.IOException -import javax.inject.Inject - -/** - * This class takes a list of images and generates a PDF file. - */ -class GeneratePDFUseCase @Inject constructor(private val logger: Logger) { - /** - * @param imagePaths list of image paths - * @return `true` if the PDF was generated successfully, `false` otherwise - */ - fun execute(imagePaths: List, filePath: String): Boolean = if (imagePaths.isEmpty() || filePath.isBlank()) { - logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath") - false - } else { - val document = PdfDocument() - fillDocumentPages(document, imagePaths) - writePdfToFile(filePath, document) - } - - /** - * @return `true` if the PDF was generated successfully, `false` otherwise - */ - private fun writePdfToFile(filePath: String, document: PdfDocument): Boolean = try { - val fileOutputStream = FileOutputStream(filePath) - document.writeTo(fileOutputStream) - fileOutputStream.close() - document.close() - true - } catch (ex: IOException) { - logger.e(TAG, "Error generating PDF", ex) - false - } - - private fun fillDocumentPages(document: PdfDocument, imagePaths: List) { - imagePaths.forEach { path -> - val bitmap = BitmapFactory.decodeFile(path) - val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, 1).create() - val page = document.startPage(pageInfo) - page.canvas.drawBitmap(bitmap, 0f, 0f, null) - document.finishPage(page) - bitmap.recycle() - } - } - - companion object { - private const val TAG = "GeneratePDFUseCase" - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt deleted file mode 100644 index 7fa343cc19b0..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Tobias Kaminsky - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.app.NotificationManager -import android.content.Context -import android.graphics.BitmapFactory -import androidx.annotation.StringRes -import androidx.core.app.NotificationCompat -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.nextcloud.client.account.AnonymousUser -import com.nextcloud.client.account.User -import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.jobs.upload.FileUploadHelper -import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.logger.Logger -import com.nextcloud.model.OCUploadLocalPathData -import com.owncloud.android.R -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.files.services.NameCollisionPolicy -import com.owncloud.android.operations.UploadFileOperation -import com.owncloud.android.ui.notifications.NotificationUtils -import com.owncloud.android.utils.theme.ViewThemeUtils -import java.io.File -import java.security.SecureRandom - -class GeneratePdfFromImagesWork( - private val appContext: Context, - private val generatePdfUseCase: GeneratePDFUseCase, - private val viewThemeUtils: ViewThemeUtils, - private val notificationManager: NotificationManager, - private val userAccountManager: UserAccountManager, - private val logger: Logger, - params: WorkerParameters -) : Worker(appContext, params) { - - override fun doWork(): Result { - val inputPaths = inputData.getStringArray(INPUT_IMAGE_FILE_PATHS)?.toList() - val outputFilePath = inputData.getString(INPUT_OUTPUT_FILE_PATH) - val uploadFolder = inputData.getString(INPUT_UPLOAD_FOLDER) - val accountName = inputData.getString(INPUT_UPLOAD_ACCOUNT) - - @Suppress("Detekt.ComplexCondition") // not that complex - require(!inputPaths.isNullOrEmpty() && outputFilePath != null && uploadFolder != null && accountName != null) { - "PDF generation work started with missing parameters:" + - " inputPaths: $inputPaths, outputFilePath: $outputFilePath," + - " uploadFolder: $uploadFolder, accountName: $accountName" - } - - val user = userAccountManager.getUser(accountName) - require(user.isPresent && user.get() !is AnonymousUser) { "Invalid or not found user" } - - logger.d( - TAG, - "PDF generation work started with parameters: inputPaths=$inputPaths," + - "outputFilePath=$outputFilePath, uploadFolder=$uploadFolder, accountName=$accountName" - ) - - val notificationId = showNotification(R.string.document_scan_pdf_generation_in_progress) - val result = generatePdfUseCase.execute(inputPaths, outputFilePath) - notificationManager.cancel(notificationId) - if (result) { - uploadFile(user.get(), uploadFolder, outputFilePath) - cleanupImages(inputPaths) - } else { - logger.w(TAG, "PDF generation failed") - showNotification(R.string.document_scan_pdf_generation_failed) - return Result.failure() - } - - logger.d(TAG, "PDF generation work finished") - return Result.success() - } - - private fun cleanupImages(inputPaths: List) { - inputPaths.forEach { - val deleted = File(it).delete() - logger.d(TAG, "Deleted $it: success = $deleted") - } - } - - private fun showNotification(@StringRes messageRes: Int): Int { - val notificationId = SecureRandom().nextInt() - val message = appContext.getString(messageRes) - - val notificationBuilder = NotificationCompat.Builder( - appContext, - NotificationUtils.NOTIFICATION_CHANNEL_GENERAL - ) - .setSmallIcon(R.drawable.notification_icon) - .setLargeIcon(BitmapFactory.decodeResource(appContext.resources, R.drawable.notification_icon)) - .setContentText(message) - .setAutoCancel(true) - - viewThemeUtils.androidx.themeNotificationCompatBuilder(appContext, notificationBuilder) - - notificationManager.notify(notificationId, notificationBuilder.build()) - - return notificationId - } - - private fun uploadFile(user: User, uploadFolder: String, pdfPath: String) { - val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name - - val data = OCUploadLocalPathData.forDocument(user, arrayOf(pdfPath), arrayOf(uploadPath)) - FileUploadHelper().uploadNewFiles(data) - } - - companion object { - const val INPUT_IMAGE_FILE_PATHS = "input_image_file_paths" - const val INPUT_OUTPUT_FILE_PATH = "input_output_file_path" - const val INPUT_UPLOAD_FOLDER = "input_upload_folder" - const val INPUT_UPLOAD_ACCOUNT = "input_upload_account" - private const val TAG = "GeneratePdfFromImagesWo" - } -} diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b1eb3fec92f1..8721a7b01efb 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -20,8 +20,6 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.client.device.PowerManagementService -import com.nextcloud.client.documentscan.GeneratePDFUseCase -import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker @@ -68,7 +66,6 @@ class BackgroundJobFactory @Inject constructor( private val deckApi: DeckApi, private val viewThemeUtils: Provider, private val localBroadcastManager: Provider, - private val generatePdfUseCase: GeneratePDFUseCase, private val syncedFolderProvider: SyncedFolderProvider, private val database: NextcloudDatabase, private val uploadFileOperationFactory: UploadFileOperationFactory @@ -105,7 +102,6 @@ class BackgroundJobFactory @Inject constructor( FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) AlbumFileUploadWorker::class -> createAlbumsFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) - GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters) @@ -291,17 +287,6 @@ class BackgroundJobFactory @Inject constructor( params ) - private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork = - GeneratePdfFromImagesWork( - appContext = context, - generatePdfUseCase = generatePdfUseCase, - viewThemeUtils = viewThemeUtils.get(), - notificationManager = notificationManager, - userAccountManager = accountManager, - logger = logger, - params = params - ) - private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork = HealthStatusWork( context, params, diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 7f4a0a3184cc..8bead0cec495 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -161,8 +161,6 @@ interface BackgroundJobManager { conflictUploadId: Long? ) - fun startPdfGenerateAndUploadWork(user: User, uploadFolder: String, imagePaths: List, pdfPath: String) - fun scheduleTestJob() fun startImmediateTestJob() fun cancelTestJob() diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 54f3852d8763..8e80712cdd0a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -27,7 +27,6 @@ import androidx.work.workDataOf import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker @@ -97,7 +96,6 @@ internal class BackgroundJobManagerImpl( const val JOB_FILES_UPLOAD = "files_upload" const val JOB_FOLDER_DOWNLOAD = "folder_download" const val JOB_FILES_DOWNLOAD = "files_download" - const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_OFFLINE_OPERATIONS = "offline_operations" @@ -826,24 +824,6 @@ internal class BackgroundJobManagerImpl( workManager.cancelAllWorkByTag(formatFileDownloadTag(accountName, fileId)) } - override fun startPdfGenerateAndUploadWork( - user: User, - uploadFolder: String, - imagePaths: List, - pdfPath: String - ) { - val data = workDataOf( - GeneratePdfFromImagesWork.INPUT_IMAGE_FILE_PATHS to imagePaths.toTypedArray(), - GeneratePdfFromImagesWork.INPUT_OUTPUT_FILE_PATH to pdfPath, - GeneratePdfFromImagesWork.INPUT_UPLOAD_ACCOUNT to user.accountName, - GeneratePdfFromImagesWork.INPUT_UPLOAD_FOLDER to uploadFolder - ) - val request = oneTimeRequestBuilder(GeneratePdfFromImagesWork::class, JOB_PDF_GENERATION) - .setInputData(data) - .build() - workManager.enqueue(request) - } - override fun scheduleTestJob() { val request = periodicRequestBuilder(TestJob::class, JOB_TEST) .setInitialDelay(DEFAULT_IMMEDIATE_JOB_DELAY_SEC, TimeUnit.SECONDS) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3d6f60bf3e1..8764dc996c49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1458,9 +1458,7 @@ Not possible without internet connection Scan page Done - Generating PDF… Error starting document scan - PDF generation failed Choose export type PDF file Multiple images diff --git a/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt b/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt index e636c304bb82..2f653570bdc2 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt @@ -20,7 +20,6 @@ import com.nextcloud.client.database.dao.FileDao import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.device.PowerManagementService -import com.nextcloud.client.documentscan.GeneratePDFUseCase import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService @@ -100,9 +99,6 @@ class BackgroundJobFactoryTest { @Mock private lateinit var localBroadcastManager: LocalBroadcastManager - @Mock - private lateinit var generatePDFUseCase: GeneratePDFUseCase - @Mock private lateinit var syncedFolderProvider: SyncedFolderProvider @@ -144,7 +140,6 @@ class BackgroundJobFactoryTest { deckApi, { viewThemeUtils }, { localBroadcastManager }, - generatePDFUseCase, syncedFolderProvider, db, uploadFileOperationFactory From e815edce8ae6843147a536d58e275d19f792c9f9 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:48:01 +0000 Subject: [PATCH 02/12] refactor(scan): remove built-in multi-page scan screen DocumentScanActivity's own page-collection UI (backed by the Hazzatur Document-Scanning-Android-SDK via AppScanOptionalFeature) is being replaced by an embedded FairScan, which handles its own multi-page capture and PDF export. Removing it here together with its only caller keeps the tree buildable; the cross-app "launch FairScan if installed separately" fallback in OCFileListBottomSheetDialog is now the sole scan path until the embedded replacement lands. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- .../android/ui/dialog/DialogFragmentIT.kt | 11 +- app/src/main/AndroidManifest.xml | 4 - .../nextcloud/client/di/ComponentsModule.kt | 4 - .../nextcloud/client/di/ViewModelModule.kt | 6 - .../documentscan/DocumentPageListAdapter.kt | 44 ---- .../documentscan/DocumentScanActivity.kt | 204 ------------------ .../documentscan/DocumentScanViewModel.kt | 188 ---------------- .../fragment/OCFileListBottomSheetActions.kt | 1 - .../fragment/OCFileListBottomSheetDialog.kt | 11 +- .../android/ui/fragment/OCFileListFragment.kt | 26 +-- .../res/layout/activity_document_scan.xml | 41 ---- .../res/layout/dialog_scan_export_type.xml | 38 ---- .../main/res/layout/document_page_item.xml | 14 -- .../main/res/menu/activity_document_scan.xml | 17 -- app/src/main/res/values/strings.xml | 6 - 15 files changed, 4 insertions(+), 611 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt delete mode 100644 app/src/main/res/layout/activity_document_scan.xml delete mode 100644 app/src/main/res/layout/dialog_scan_export_type.xml delete mode 100644 app/src/main/res/layout/document_page_item.xml delete mode 100644 app/src/main/res/menu/activity_document_scan.xml diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt index dd06bdc081f9..12cc4f636a88 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt @@ -19,7 +19,6 @@ import android.os.Looper import android.view.ViewGroup import android.webkit.SslErrorHandler import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContract import androidx.fragment.app.DialogFragment import androidx.test.core.app.ActivityScenario import androidx.test.core.app.launchActivity @@ -37,7 +36,6 @@ import com.nextcloud.android.lib.resources.profile.HoverCard import com.nextcloud.client.account.RegisteredUser import com.nextcloud.client.account.Server import com.nextcloud.client.device.DeviceInfo -import com.nextcloud.client.documentscan.AppScanOptionalFeature import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment.Companion.newInstance import com.nextcloud.ui.dialog.account.onlineStatus.SetOnlineStatusBottomSheet @@ -432,7 +430,6 @@ class DialogFragmentIT : AbstractIT() { override fun newSpreadsheet() = Unit override fun newPresentation() = Unit override fun directCameraUpload() = Unit - override fun scanDocUpload() = Unit override fun scanDocUploadFromApp() = Unit override val isScanDocUploadFromAppAvailable: Boolean get() = false @@ -502,11 +499,6 @@ class DialogFragmentIT : AbstractIT() { } CapabilityUtils.updateCapability(capability) - val appScanOptionalFeature: AppScanOptionalFeature = object : AppScanOptionalFeature() { - override fun getScanContract(): ActivityResultContract = - throw UnsupportedOperationException("Document scan is not available") - } - val viewThemeUtils = ViewThemeUtils( materialSchemesForCurrentUser, ColorUtil(targetContext) @@ -522,8 +514,7 @@ class DialogFragmentIT : AbstractIT() { ocFile, fda.themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature + editorUtils ) sut.show() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8014d33ba07c..477f325ea489 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -670,10 +670,6 @@ android:name=".ui.preview.pdf.PreviewBitmapActivity" android:exported="false" android:theme="@style/Theme.ownCloud.OverlayGrey" /> - - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import coil.load -import com.owncloud.android.databinding.DocumentPageItemBinding - -class DocumentPageListAdapter : - ListAdapter(DiffItemCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocumentPageViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = DocumentPageItemBinding.inflate(inflater, parent, false) - return DocumentPageViewHolder(binding) - } - - override fun onBindViewHolder(holder: DocumentPageViewHolder, position: Int) { - holder.bind(currentList[position]) - } - - override fun getItemCount(): Int = currentList.size - - class DocumentPageViewHolder(val binding: DocumentPageItemBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(imagePath: String) { - binding.root.load(imagePath) - } - } - - private class DiffItemCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem - - override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt deleted file mode 100644 index a545cee8acc4..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.os.Bundle -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AlertDialog -import androidx.core.view.MenuProvider -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.nextcloud.client.di.Injectable -import com.nextcloud.client.di.ViewModelFactory -import com.nextcloud.client.logger.Logger -import com.owncloud.android.R -import com.owncloud.android.databinding.ActivityDocumentScanBinding -import com.owncloud.android.databinding.DialogScanExportTypeBinding -import com.owncloud.android.ui.activity.ToolbarActivity -import com.owncloud.android.ui.navigation.model.ActionBarStyle -import javax.inject.Inject - -class DocumentScanActivity : - ToolbarActivity(), - Injectable { - - @Inject - lateinit var vmFactory: ViewModelFactory - - @Inject - lateinit var logger: Logger - - @Inject - lateinit var appScanOptionalFeature: AppScanOptionalFeature - - lateinit var binding: ActivityDocumentScanBinding - - lateinit var viewModel: DocumentScanViewModel - - private var scanPage: ActivityResultLauncher? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - scanPage = registerForActivityResult(appScanOptionalFeature.getScanContract()) { result -> - viewModel.onScanPageResult(result) - } - - val folder = intent.extras?.getString(EXTRA_FOLDER) - require(folder != null) { "Folder must be provided for upload" } - - viewModel = ViewModelProvider(this, vmFactory)[DocumentScanViewModel::class.java] - viewModel.setUploadFolder(folder) - - setupViews() - - observeState() - } - - private fun setupViews() { - binding = ActivityDocumentScanBinding.inflate(layoutInflater) - setContentView(binding.root) - - showActionBar(ActionBarStyle.Plain) - supportActionBar?.let { - it.setDisplayHomeAsUpEnabled(true) - it.setDisplayShowHomeEnabled(true) - viewThemeUtils.files.themeActionBar(this, it) - } - - viewThemeUtils.material.themeFAB(binding.fab) - binding.fab.setOnClickListener { - viewModel.onAddPageClicked() - } - - binding.pagesRecycler.layoutManager = GridLayoutManager(this, PAGE_COLUMNS) - - setupMenu() - } - - private fun setupMenu() { - addMenuProvider( - object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.activity_document_scan, menu) - menu.findItem(R.id.action_save)?.let { - viewThemeUtils.platform.colorToolbarMenuIcon(this@DocumentScanActivity, it) - } - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.action_save -> { - viewModel.onClickDone() - true - } - - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - true - } - - else -> false - } - } - ) - } - - private fun observeState() { - viewModel.uiState.observe(this, ::handleState) - } - - private fun handleState(state: DocumentScanViewModel.UIState) { - logger.d(TAG, "handleState: called with $state") - when (state) { - is DocumentScanViewModel.UIState.BaseState -> when (state) { - is DocumentScanViewModel.UIState.NormalState -> { - updateButtonsEnabled(true) - val pageList = state.pageList - updateRecycler(pageList) - if (state.shouldRequestScan) { - startPageScan() - } - } - - is DocumentScanViewModel.UIState.RequestExportState -> { - updateButtonsEnabled(false) - if (state.shouldRequestExportType) { - showExportDialog() - viewModel.onRequestTypeHandled() - } - } - } - - DocumentScanViewModel.UIState.DoneState, DocumentScanViewModel.UIState.CanceledState -> { - finish() - } - } - } - - private fun showExportDialog() { - val dialogBinding = DialogScanExportTypeBinding.inflate(layoutInflater) - - val dialog = MaterialAlertDialogBuilder(this) - .setTitle(R.string.document_scan_export_dialog_title) - .setCancelable(true) - .setView(dialogBinding.root) - .setNegativeButton(R.string.common_cancel) { _, _ -> - viewModel.onExportCanceled() - } - .setOnCancelListener { viewModel.onExportCanceled() } - .also { - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this@DocumentScanActivity, it) - } - .create() - - viewThemeUtils.platform.colorTextButtons(dialogBinding.btnPdf, dialogBinding.btnImages) - - dialogBinding.btnPdf.setOnClickListener { - viewModel.onExportTypeSelected(DocumentScanViewModel.ExportType.PDF) - dialog.dismiss() - } - dialogBinding.btnImages.setOnClickListener { - viewModel.onExportTypeSelected(DocumentScanViewModel.ExportType.IMAGES) - dialog.dismiss() - } - - dialog.setOnShowListener { - val alertDialog = it as AlertDialog - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)) - } - - dialog.show() - } - - private fun updateRecycler(pageList: List) { - if (binding.pagesRecycler.adapter == null) { - binding.pagesRecycler.adapter = DocumentPageListAdapter() - } - (binding.pagesRecycler.adapter as? DocumentPageListAdapter)?.submitList(pageList) - } - - private fun updateButtonsEnabled(enabled: Boolean) { - binding.fab.isEnabled = enabled - } - - private fun startPageScan() { - logger.d(TAG, "startPageScan() called") - viewModel.onScanRequestHandled() - scanPage!!.launch(Unit) - } - - companion object { - private const val TAG = "DocumentScanActivity" - private const val PAGE_COLUMNS = 2 - const val EXTRA_FOLDER = "extra_folder" - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt deleted file mode 100644 index a6a5d5062f46..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.nextcloud.client.account.CurrentAccountProvider -import com.nextcloud.client.di.IoDispatcher -import com.nextcloud.client.jobs.BackgroundJobManager -import com.nextcloud.client.jobs.upload.FileUploadHelper -import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.logger.Logger -import com.nextcloud.model.OCUploadLocalPathData -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.files.services.NameCollisionPolicy -import com.owncloud.android.operations.UploadFileOperation -import com.owncloud.android.ui.helpers.FileOperationsHelper -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.io.File -import javax.inject.Inject - -class DocumentScanViewModel @Inject constructor( - @IoDispatcher private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - app: Application, - private val logger: Logger, - private val backgroundJobManager: BackgroundJobManager, - private val currentAccountProvider: CurrentAccountProvider -) : AndroidViewModel(app) { - init { - logger.d(TAG, "DocumentScanViewModel created") - } - - sealed interface UIState { - sealed class BaseState(val pageList: List) : UIState { - val isEmpty: Boolean - get() = pageList.isEmpty() - } - - class NormalState(pageList: List = emptyList(), val shouldRequestScan: Boolean = false) : - BaseState(pageList) - - class RequestExportState(pageList: List = emptyList(), val shouldRequestExportType: Boolean = true) : - BaseState(pageList) - - object DoneState : UIState - object CanceledState : UIState - } - - private var uploadFolder: String? = null - private val initialState = UIState.NormalState(shouldRequestScan = true) - private val _uiState = MutableLiveData(initialState) - val uiState: LiveData - get() = _uiState - - /** - * @param result should be the path to the scanned page on the disk - */ - fun onScanPageResult(result: String?) { - logger.d(TAG, "onScanPageResult() called with: result = $result") - - val state = _uiState.value - require(state is UIState.NormalState) - - viewModelScope.launch(ioDispatcher) { - if (result != null) { - val newPath = renameCapturedImage(result) - val pageList = state.pageList.toMutableList() - pageList.add(newPath) - _uiState.postValue(UIState.NormalState(pageList)) - } else { - // result == null means cancellation or error - if (state.isEmpty) { - // close only if no pages have been added yet - _uiState.postValue(UIState.CanceledState) - } - } - } - } - - // TODO extract to usecase - private fun renameCapturedImage(originalPath: String): String { - val file = File(originalPath) - val renamedFile = - File( - getApplication().cacheDir.path + - File.separator + FileOperationsHelper.getCapturedImageName() - ) - file.renameTo(renamedFile) - return renamedFile.absolutePath - } - - fun onScanRequestHandled() { - val state = uiState.value - require(state is UIState.NormalState) - - _uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = false)) - } - - fun onAddPageClicked() { - val state = uiState.value - require(state is UIState.NormalState) - if (!state.shouldRequestScan) { - _uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = true)) - } - } - - fun onClickDone() { - val state = _uiState.value - if (state is UIState.BaseState && !state.isEmpty) { - _uiState.postValue(UIState.RequestExportState(state.pageList)) - } - } - - fun setUploadFolder(folder: String) { - this.uploadFolder = folder - } - - fun onRequestTypeHandled() { - val state = _uiState.value - require(state is UIState.RequestExportState) - _uiState.postValue(UIState.RequestExportState(state.pageList, false)) - } - - fun onExportTypeSelected(exportType: ExportType) { - val state = _uiState.value - require(state is UIState.RequestExportState) - when (exportType) { - ExportType.PDF -> { - exportToPdf(state.pageList) - } - - ExportType.IMAGES -> { - exportToImages(state.pageList) - } - } - _uiState.postValue(UIState.DoneState) - } - - private fun exportToPdf(pageList: List) { - val genPath = - getApplication().cacheDir.path + File.separator + FileOperationsHelper.getTimestampedFileName( - ".pdf" - ) - backgroundJobManager.startPdfGenerateAndUploadWork( - currentAccountProvider.user, - uploadFolder!!, - pageList, - genPath - ) - // after job is started, finish the application. - _uiState.postValue(UIState.DoneState) - } - - private fun exportToImages(pageList: List) { - val uploadPaths = pageList.map { - uploadFolder + OCFile.PATH_SEPARATOR + File(it).name - }.toTypedArray() - - val data = OCUploadLocalPathData.forDocument(currentAccountProvider.user, pageList.toTypedArray(), uploadPaths) - FileUploadHelper.instance().uploadNewFiles(data) - } - - fun onExportCanceled() { - val state = _uiState.value - if (state is UIState.BaseState) { - _uiState.postValue(UIState.NormalState(state.pageList)) - } - } - - private companion object { - private const val TAG = "DocumentScanViewModel" - } - - enum class ExportType { - PDF, - IMAGES - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt index 5f8a7df59994..e0d9a66b819d 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt @@ -33,7 +33,6 @@ interface OCFileListBottomSheetActions { fun newPresentation() fun directCameraUpload() - fun scanDocUpload() /** * Offers scanning a document in a supported external app and then upload to the current folder. diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt index 22d4f255a9fb..6b311b40c79c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt @@ -28,7 +28,6 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.AppScanOptionalFeature import com.nextcloud.utils.BuildHelper.isFlavourGPlay import com.nextcloud.utils.EditorUtils import com.nextcloud.utils.extensions.isNetworkAndServerAvailableSuspended @@ -58,8 +57,7 @@ class OCFileListBottomSheetDialog( private val file: OCFile, private val themeUtils: ThemeUtils, private val viewThemeUtils: ViewThemeUtils, - private val editorUtils: EditorUtils, - private val appScanOptionalFeature: AppScanOptionalFeature + private val editorUtils: EditorUtils ) : BottomSheetDialog(fileActivity), Injectable { @@ -396,12 +394,7 @@ class OCFileListBottomSheetDialog( dismiss() } - if (appScanOptionalFeature.isAvailable) { - menuScanDocUpload.setOnClickListener { - actions.scanDocUpload() - dismiss() - } - } else if (actions.isScanDocUploadFromAppAvailable) { + if (actions.isScanDocUploadFromAppAvailable) { menuScanDocUpload.setOnClickListener { actions.scanDocUploadFromApp() dismiss() diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 5e80cd21e333..8f71f9e91969 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -43,8 +43,6 @@ import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation import com.nextcloud.client.account.User import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.AppScanOptionalFeature -import com.nextcloud.client.documentscan.DocumentScanActivity import com.nextcloud.client.utils.Throttler import com.nextcloud.repository.ClientRepository import com.nextcloud.ui.fileactions.FileAction @@ -157,9 +155,6 @@ open class OCFileListFragment : @Inject lateinit var fileActionHandler: FileActionHandler - @Inject - lateinit var appScanOptionalFeature: AppScanOptionalFeature - @Inject lateinit var thumbnailGenerator: ThumbnailGenerator @@ -473,8 +468,7 @@ open class OCFileListFragment : currentDir, themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature + editorUtils ).run { behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.skipCollapsed = true @@ -574,24 +568,6 @@ open class OCFileListFragment : builder.show() } - override fun scanDocUpload() { - val fileDisplayActivity = activity as? FileDisplayActivity - val currentFile = currentFile - if (fileDisplayActivity == null || currentFile == null || !currentFile.isFolder) { - Log_OC.w( - TAG, - "Cannot start document scan: activity=${activity?.javaClass?.simpleName}, " + - "folder=${currentFile?.remotePath}, isFolder=${currentFile?.isFolder}" - ) - SnackbarUtil.show(this, R.string.error_starting_doc_scan) - return - } - - val intent = Intent(requireContext(), DocumentScanActivity::class.java) - .putExtra(DocumentScanActivity.EXTRA_FOLDER, currentFile.remotePath) - startActivity(intent) - } - @Suppress("DEPRECATION") override fun scanDocUploadFromApp() { requireActivity().startActivityForResult( diff --git a/app/src/main/res/layout/activity_document_scan.xml b/app/src/main/res/layout/activity_document_scan.xml deleted file mode 100644 index 11e8d9e74acf..000000000000 --- a/app/src/main/res/layout/activity_document_scan.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_scan_export_type.xml b/app/src/main/res/layout/dialog_scan_export_type.xml deleted file mode 100644 index 592a50e1a884..000000000000 --- a/app/src/main/res/layout/dialog_scan_export_type.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/document_page_item.xml b/app/src/main/res/layout/document_page_item.xml deleted file mode 100644 index 4ce3795b4c12..000000000000 --- a/app/src/main/res/layout/document_page_item.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/app/src/main/res/menu/activity_document_scan.xml b/app/src/main/res/menu/activity_document_scan.xml deleted file mode 100644 index 7297fb0d67af..000000000000 --- a/app/src/main/res/menu/activity_document_scan.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8764dc996c49..5df5197b53b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1456,12 +1456,6 @@ Add end-to-end encryption to this client Add new secure file drop Not possible without internet connection - Scan page - Done - Error starting document scan - Choose export type - PDF file - Multiple images Cannot create local file Invalid filename for local file Team folders From b9576677e9120b4f95affa535cb40a9ef3585bd1 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:49:12 +0000 Subject: [PATCH 03/12] refactor(scan): remove :appscan module and its reflection wiring Nothing references AppScanOptionalFeature/VariantModule's reflection lookup of com.nextcloud.appscan.ScanPageContract anymore, so the Hazzatur Document-Scanning-Android-SDK dependency and the :appscan module it lived in go too. The JitPack repo in settings.gradle.kts stays - other dependencies still resolve through it. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- app/build.gradle.kts | 14 -- .../nextcloud/client/di/VariantModuleTest.kt | 50 ------- .../nextcloud/client/di/VariantModuleTest.kt | 47 ------- .../com/nextcloud/client/di/AppComponent.kt | 1 - .../com/nextcloud/client/di/VariantModule.kt | 45 ------- .../documentscan/AppScanOptionalFeature.kt | 29 ----- appscan/build.gradle.kts | 48 ------- appscan/src/main/AndroidManifest.xml | 19 --- .../com/nextcloud/appscan/AppScanActivity.kt | 122 ------------------ .../com/nextcloud/appscan/ScanPageContract.kt | 27 ---- gradle/libs.versions.toml | 2 - settings.gradle.kts | 2 +- 12 files changed, 1 insertion(+), 405 deletions(-) delete mode 100644 app/src/androidTestGeneric/java/com/nextcloud/client/di/VariantModuleTest.kt delete mode 100644 app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt delete mode 100644 app/src/main/java/com/nextcloud/client/di/VariantModule.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt delete mode 100644 appscan/build.gradle.kts delete mode 100644 appscan/src/main/AndroidManifest.xml delete mode 100644 appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt delete mode 100644 appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c822052f1d13..fbb7b940c98b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -337,13 +337,6 @@ tasks.named("check").configure { val kspConfiguration = "ksp" val kspAndroidTestConfiguration = "kspAndroidTest" val gplayImplementationConfiguration = "gplayImplementation" -val huaweiImplementationConfiguration = "huaweiImplementation" -val qaImplementationConfiguration = "qaImplementation" -val appScanConfigurations = listOf( - gplayImplementationConfiguration, - huaweiImplementationConfiguration, - qaImplementationConfiguration -) dependencies { // region Nextcloud library @@ -439,13 +432,6 @@ dependencies { implementation(libs.emoji.google) // endregion - // region AppScan, document scanner not available on FDroid (generic) due to OpenCV binaries - // To enable the feature for another variant, add its "Implementation" here. - appScanConfigurations.forEach { configuration -> - add(configuration, project(":appscan")) - } - // endregion - // region SpotBugs spotbugsPlugins(libs.findsecbugs.plugin) spotbugsPlugins(libs.fb.contrib) diff --git a/app/src/androidTestGeneric/java/com/nextcloud/client/di/VariantModuleTest.kt b/app/src/androidTestGeneric/java/com/nextcloud/client/di/VariantModuleTest.kt deleted file mode 100644 index ed7d07d4db34..000000000000 --- a/app/src/androidTestGeneric/java/com/nextcloud/client/di/VariantModuleTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.di - -import com.nextcloud.client.documentscan.AppScanOptionalFeature -import dagger.Component -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -/** - * Unit test for [VariantModule] that tests the reflection-based approach - * to conditionally load the ScanPageContract. - */ -class VariantModuleTest { - - private lateinit var component: TestVariantComponent - - @Before - fun setup() { - component = DaggerVariantModuleTest_TestVariantComponent.create() - } - - @Test - fun testAppScanWhenNotAvailableShouldReturnError() { - val feature = component.appScanOptionalFeature() - - assertFalse(feature.isAvailable) - assertEquals(AppScanOptionalFeature.Stub, feature) - - try { - feature.getScanContract() - throw AssertionError("Expected UnsupportedOperationException") - } catch (e: UnsupportedOperationException) { - assertTrue(e.message?.contains("not available") == true) - } - } - - @Component(modules = [VariantModule::class]) - interface TestVariantComponent { - fun appScanOptionalFeature(): AppScanOptionalFeature - } -} diff --git a/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt b/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt deleted file mode 100644 index 3ac80ebe9d6c..000000000000 --- a/app/src/androidTestGplay/java/com/nextcloud/client/di/VariantModuleTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.di - -import com.nextcloud.client.documentscan.AppScanOptionalFeature -import dagger.Component -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -/** - * Unit test for [VariantModule] that tests the reflection-based approach - * to conditionally load the ScanPageContract. - */ -class VariantModuleTest { - - private lateinit var component: TestVariantComponent - - @Before - fun setup() { - component = DaggerVariantModuleTest_TestVariantComponent.create() - } - - @Test - fun testAppScanWhenAvailableShouldReturnContract() { - val feature = component.appScanOptionalFeature() - - assertTrue(feature.isAvailable) - assertNotEquals(AppScanOptionalFeature.Stub, feature) - - assertNotNull(feature.getScanContract()) - } - - @Component(modules = [VariantModule::class]) - interface TestVariantComponent { - fun appScanOptionalFeature(): AppScanOptionalFeature - } -} diff --git a/app/src/main/java/com/nextcloud/client/di/AppComponent.kt b/app/src/main/java/com/nextcloud/client/di/AppComponent.kt index 75efe13470f8..e9e680634f52 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppComponent.kt +++ b/app/src/main/java/com/nextcloud/client/di/AppComponent.kt @@ -47,7 +47,6 @@ import javax.inject.Singleton ThemeModule::class, DatabaseModule::class, DispatcherModule::class, - VariantModule::class, PlayerModule::class ] ) diff --git a/app/src/main/java/com/nextcloud/client/di/VariantModule.kt b/app/src/main/java/com/nextcloud/client/di/VariantModule.kt deleted file mode 100644 index 78899be6aec7..000000000000 --- a/app/src/main/java/com/nextcloud/client/di/VariantModule.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Philipp Hasper - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.di - -import androidx.activity.result.contract.ActivityResultContract -import com.nextcloud.client.documentscan.AppScanOptionalFeature -import dagger.Module -import dagger.Provides -import dagger.Reusable - -@Module -internal class VariantModule { - /** - * Using reflection to determine whether the ScanPageContract class from the appscan project is available. - * If yes, an instance of it is returned. If not, a stub is returned indicating the feature is not available. - * - * To make it available for your specific variant, make sure it is included in your build.gradle, - * e.g.: `"qaImplementation"(project(":appscan"))` - */ - @Provides - @Reusable - fun scanOptionalFeature(): AppScanOptionalFeature = try { - // Try to load the ScanPageContract class only if the appscan project is present - val clazz = Class.forName("com.nextcloud.appscan.ScanPageContract") - - @Suppress("UNCHECKED_CAST") - val contractInstance = - clazz.getDeclaredConstructor().newInstance() as ActivityResultContract - object : AppScanOptionalFeature() { - override fun getScanContract(): ActivityResultContract = contractInstance - } - } catch (_: ClassNotFoundException) { - // appscan module is not present in this variant - AppScanOptionalFeature.Stub - } catch (_: Exception) { - // Any reflection/instantiation error -> be safe and use stub - AppScanOptionalFeature.Stub - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt b/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt deleted file mode 100644 index d6996e17fcd5..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import androidx.activity.result.contract.ActivityResultContract - -abstract class AppScanOptionalFeature { - /** - * Check [isAvailable] before calling this method. - */ - abstract fun getScanContract(): ActivityResultContract - open val isAvailable: Boolean = true - - /** - * Use this in variants where the feature is not available - */ - @Suppress("unused") // used only in some variants - object Stub : AppScanOptionalFeature() { - override fun getScanContract(): ActivityResultContract = - throw UnsupportedOperationException("Document scan is not available") - - override val isAvailable = false - } -} diff --git a/appscan/build.gradle.kts b/appscan/build.gradle.kts deleted file mode 100644 index 1ade447dfb0b..000000000000 --- a/appscan/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) -} - -android { - namespace = "com.nextcloud.appscan" - - defaultConfig { - minSdk = 28 - compileSdk = 37 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - - lint.targetSdk = 36 - testOptions.targetSdk = 36 -} - -kotlin.compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") -} - -dependencies { - implementation(libs.appcompat) - implementation(libs.document.scanning.android.sdk) { - exclude(group = "androidx.test", module = "monitor") - } - implementation(libs.ui) -} diff --git a/appscan/src/main/AndroidManifest.xml b/appscan/src/main/AndroidManifest.xml deleted file mode 100644 index df434e2fe8be..000000000000 --- a/appscan/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - diff --git a/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt b/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt deleted file mode 100644 index 58d3b4a99e50..000000000000 --- a/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.appscan - -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.PointF -import android.graphics.drawable.BitmapDrawable -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.core.view.children -import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.ScanActivity -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.ScannerResults -import com.zynksoftware.documentscanner.ui.DocumentScanner - -@Suppress("unused") -class AppScanActivity : ScanActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - applyEdgeToEdgeWithSystemBarPadding() - super.onCreate(null) - DocumentScanner.init(this) - addFragmentContentLayout() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - recreateCropFragmentView() - } - - private fun recreateCropFragmentView() { - val cropFragment = supportFragmentManager.findFragmentByTag(IMAGE_CROP_FRAGMENT_TAG) ?: return - if (!cropFragment.isVisible) { - return - } - - val cornerFractions = cropFragment.view?.let(::cropCornerFractions) - - supportFragmentManager.beginTransaction().detach(cropFragment).commitNow() - supportFragmentManager.beginTransaction().attach(cropFragment).commitNow() - - val cropView = cropFragment.view ?: return - if (cornerFractions == null) { - return - } - - cropView.post { restoreCropCorners(cropView, cornerFractions) } - } - - private fun cropCornerFractions(cropView: View): List? { - val preview = cropPreviewBitmap(cropView) ?: return null - return cropCornerHandles(cropView).map { PointF(it.x / preview.width, it.y / preview.height) } - } - - private fun restoreCropCorners(cropView: View, cornerFractions: List) { - val preview = cropPreviewBitmap(cropView) ?: return - cropCornerHandles(cropView).zip(cornerFractions) { handle, fraction -> - handle.x = fraction.x * preview.width - handle.y = fraction.y * preview.height - } - cropView.findViewById(R.id.polygonView).invalidate() - } - - private fun cropPreviewBitmap(cropView: View): Bitmap? = - (cropView.findViewById(R.id.imagePreview).drawable as? BitmapDrawable)?.bitmap - - private fun cropCornerHandles(cropView: View): List = - cropView.findViewById(R.id.polygonView).children.toList() - - override fun onError(error: DocumentScannerErrorModel) = Unit - - override fun onSuccess(scannerResults: ScannerResults) { - val intent = Intent() - - intent.putExtra( - EXTRA_FILE, - scannerResults.transformedImageFile?.absolutePath ?: scannerResults.croppedImageFile?.absolutePath - ) - - setResult(RESULT_OK, intent) - finish() - } - - override fun onClose() { - if (isChangingConfigurations) { - return - } - - setResult(RESULT_CANCELED) - finish() - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (permissions.contains(android.Manifest.permission.CAMERA) && - (grantResults.isEmpty() || grantResults[0] == PackageManager.PERMISSION_DENIED) - ) { - onClose() - } - } - - companion object { - @JvmStatic - val enabled: Boolean = true - - const val EXTRA_FILE = "file" - - private const val IMAGE_CROP_FRAGMENT_TAG = "ImageCropFragmentTag" - } -} diff --git a/appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt b/appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt deleted file mode 100644 index 2170bc0be9b6..000000000000 --- a/appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.appscan // Note: if class package or name changes, you must adjust the app's VariantModule.kt - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContract - -@Suppress("unused") // Class is instantiated via reflection -class ScanPageContract : ActivityResultContract() { - override fun createIntent(context: Context, input: Unit): Intent { - return Intent(context, AppScanActivity::class.java) - } - - override fun parseResult(resultCode: Int, intent: Intent?): String? { - if (resultCode != Activity.RESULT_OK) { - return null - } - return intent?.getStringExtra(AppScanActivity.EXTRA_FILE) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6ce3a3298b6..21475dbaf5d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,6 @@ coreVersion = "0.15.0" daggerVersion = "2.60.1" detektGradlePlugin = "1.23.8" disklrucacheVersion = "2.0.2" -documentScannerVersion = "1.3.0" emojiGoogleVersion = "0.24.1" espressoVersion = "3.7.0" eventbusVersion = "3.3.1" @@ -94,7 +93,6 @@ bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bc browser = { module = "androidx.browser:browser", version.ref = "browserVersion" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appCompatVersion" } core-ktx = { module = "androidx.test:core-ktx", version.ref = "androidxTestVersion" } -document-scanning-android-sdk = { module = "com.github.Hazzatur:Document-Scanning-Android-SDK", version.ref = "documentScannerVersion" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtxVersion" } exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterfaceVersion" } material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCoreVersion" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 26e4b55708b4..80f22edeedaf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -66,4 +66,4 @@ includeBuild("../android-common") { } */ -include(":app", ":appscan") +include(":app") From 4eda320c2a09962db26430e08dfbe03d378cfd22 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Thu, 24 Sep 2026 11:31:32 +0200 Subject: [PATCH 04/12] feat(scan): Include FairScan via mavenLocal (WIP) FairScan will replace the old scanner component with state-of-the-art CV and UI. This is WIP, because FairScan is (not yet) available on a remote registry. So it is only included via local maven, and the CI builds it via ./gradlew publishToMavenLocal Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- .../action.yml | 44 +++++++++++++++++++ .github/workflows/analysis.yml | 1 + .github/workflows/assembleFlavors.yml | 1 + .github/workflows/check.yml | 1 + .github/workflows/detectWrongSettings.yml | 1 + .github/workflows/garm.yml | 2 + .github/workflows/qa.yml | 4 ++ .github/workflows/unit-tests.yml | 2 + app/build.gradle.kts | 4 ++ app/src/main/AndroidManifest.xml | 10 ++++- gradle.properties | 6 +++ gradle/libs.versions.toml | 3 ++ settings.gradle.kts | 11 +++++ 13 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .github/actions/publish-fairscan-to-maven-local/action.yml diff --git a/.github/actions/publish-fairscan-to-maven-local/action.yml b/.github/actions/publish-fairscan-to-maven-local/action.yml new file mode 100644 index 000000000000..a95492172f3b --- /dev/null +++ b/.github/actions/publish-fairscan-to-maven-local/action.yml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + +name: "Publish FairScan to mavenLocal" +description: > + Checks out FairScan and publishes it to mavenLocal so a subsequent Gradle build of :app in the + SAME job can resolve org.fairscan:app from mavenLocal(), as configured in settings.gradle.kts. + Must run in the same job as the nextcloud-android build (writes to ~/.m2/repository on the + runner's filesystem) and after a JDK 17-21 has already been set up by the calling workflow + (FairScan's Gradle 8.13 wrapper needs JDK 17-21; JDK 25 fails). + +inputs: + repository: + description: "FairScan repository to check out (owner/repo)." + required: false + # TEMPORARY: points at a fork+branch, not a tagged FairScan release. FairScan isn't published + # anywhere (no JitPack, no release with the new library shape) yet, so CI builds it from + # source. Update this default (and settings.gradle.kts' mavenLocal repository, and + # gradle/libs.versions.toml's fairscanVersion) once FairScan publishes a real, versioned release. + default: "phillab/FairScan" + ref: + description: "FairScan ref to build. See the 'repository' input comment: temporary." + required: false + default: "ph/offer_as_library" + path: + description: "Directory (relative to the job workspace) to check FairScan out into." + required: false + default: "fairscan-src" + +runs: + using: "composite" + steps: + - name: Checkout FairScan + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + path: ${{ inputs.path }} + persist-credentials: false + + - name: Publish FairScan to mavenLocal + shell: bash + working-directory: ${{ inputs.path }} + run: ./gradlew --no-daemon publishToMavenLocal diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 067a8a663cd0..c7bed201cffa 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -61,6 +61,7 @@ jobs: with: distribution: "temurin" java-version: 25 + - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Install dependencies run: | sudo apt install python3-defusedxml diff --git a/.github/workflows/assembleFlavors.yml b/.github/workflows/assembleFlavors.yml index 3c77066448d7..1067b63175a2 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -40,6 +40,7 @@ jobs: - uses: gradle/actions/wrapper-validation@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - name: Setup Gradle uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 + - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Build ${{ matrix.flavor }} run: | { diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 212ec4adef78..a0a697c90a7c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -32,5 +32,6 @@ jobs: java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 + - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Check ${{ matrix.task }} run: ./gradlew ${{ matrix.task }} diff --git a/.github/workflows/detectWrongSettings.yml b/.github/workflows/detectWrongSettings.yml index 09d2cdfb0f14..6c1073e112c7 100644 --- a/.github/workflows/detectWrongSettings.yml +++ b/.github/workflows/detectWrongSettings.yml @@ -26,5 +26,6 @@ jobs: with: distribution: "temurin" java-version: 21 + - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Detect SNAPSHOT run: scripts/analysis/detectWrongSettings.sh diff --git a/.github/workflows/garm.yml b/.github/workflows/garm.yml index c974bbaa257c..85b513387ea1 100644 --- a/.github/workflows/garm.yml +++ b/.github/workflows/garm.yml @@ -50,6 +50,8 @@ jobs: distribution: "temurin" java-version: 21 + - uses: ./.github/actions/publish-fairscan-to-maven-local + - name: Configure gradle daemon run: | mkdir -p $HOME/.gradle diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index e6b5696cda99..13ac39ecce3a 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -37,6 +37,10 @@ jobs: distribution: "temurin" java-version: 25 + - name: Publish FairScan to mavenLocal + if: ${{ steps.check-secrets.outputs.ok == 'true' }} + uses: ./.github/actions/publish-fairscan-to-maven-local + - name: Build QA id: build-qa if: ${{ steps.check-secrets.outputs.ok == 'true' }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 2129695b32c3..5be789e1f7c5 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -39,6 +39,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 + - uses: ./.github/actions/publish-fairscan-to-maven-local + - name: Run unit tests with coverage run: ./gradlew jacocoTestGplayDebugUnitTest diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fbb7b940c98b..163c6e2f29cd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -432,6 +432,10 @@ dependencies { implementation(libs.emoji.google) // endregion + // region FairScan + implementation(libs.fairscan) + // endregion + // region SpotBugs spotbugsPlugins(libs.findsecbugs.plugin) spotbugsPlugins(libs.fb.contrib) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 477f325ea489..84c1e4a9fbd1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,8 @@ + tools:ignore="ScopedStorage" + tools:replace="android:maxSdkVersion" /> @@ -131,6 +132,13 @@ android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_config" /> + + + + Date: Thu, 24 Sep 2026 22:45:58 +0200 Subject: [PATCH 05/12] feat(scan): Start onboard FairScan component, if external app not installed A separately installed FairScan wins over the built-in scanner, so users can choose to install (a newer version of) FairScan themselves. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- .../android/ui/dialog/DialogFragmentIT.kt | 2 -- .../fragment/OCFileListBottomSheetActions.kt | 8 ++----- .../fragment/OCFileListBottomSheetDialog.kt | 11 +++------- .../android/ui/fragment/OCFileListFragment.kt | 21 ++++++++++++------- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt index 12cc4f636a88..c7c88229ad46 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt @@ -431,8 +431,6 @@ class DialogFragmentIT : AbstractIT() { override fun newPresentation() = Unit override fun directCameraUpload() = Unit override fun scanDocUploadFromApp() = Unit - override val isScanDocUploadFromAppAvailable: Boolean - get() = false override fun showTemplate(creator: Creator?, headline: String?) = Unit override fun createRichWorkspace() = Unit } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt index e0d9a66b819d..8869d77e044f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt @@ -35,15 +35,11 @@ interface OCFileListBottomSheetActions { fun directCameraUpload() /** - * Offers scanning a document in a supported external app and then upload to the current folder. + * Scans a document (in a separately installed FairScan if present, the built-in scanner + * otherwise) and uploads it to the current folder. */ fun scanDocUploadFromApp() - /** - * @return true, if a supported external app is available for [.scanDocUploadFromApp] - */ - val isScanDocUploadFromAppAvailable: Boolean - /** * open template selection for creator @link Creator */ diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt index 6b311b40c79c..559fb2eafecc 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt @@ -394,14 +394,9 @@ class OCFileListBottomSheetDialog( dismiss() } - if (actions.isScanDocUploadFromAppAvailable) { - menuScanDocUpload.setOnClickListener { - actions.scanDocUploadFromApp() - dismiss() - } - } else { - menuScanDocUpload.visibility = View.GONE - menuScanDocUploadDivider.visibility = View.GONE + menuScanDocUpload.setOnClickListener { + actions.scanDocUploadFromApp() + dismiss() } menuUploadFiles.setOnClickListener { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 8f71f9e91969..3ca18dbd3734 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -115,6 +115,7 @@ import com.owncloud.android.utils.theme.ThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.fairscan.app.FairScan import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -570,18 +571,22 @@ open class OCFileListFragment : @Suppress("DEPRECATION") override fun scanDocUploadFromApp() { - requireActivity().startActivityForResult( - scanIntentExternalApp, + val activity = requireActivity() + + // A separately installed FairScan wins over the built-in scanner, so users can pick up + // a newer FairScan release before Nextcloud itself updates the bundled version. + val scanIntent = if (scanIntentExternalApp.resolveActivity(activity.packageManager) != null) { + scanIntentExternalApp + } else { + FairScan.scanToPdfIntent(activity) + } + + activity.startActivityForResult( + scanIntent, FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME ) } - override val isScanDocUploadFromAppAvailable: Boolean - get() { - val context = activity ?: return false - return scanIntentExternalApp.resolveActivity(context.packageManager) != null - } - override fun uploadFiles() { val fileActivity = activity as? FileActivity if (fileActivity == null) { From 2d6f7226aa9114355d9b288b902b272ce5bf42ca Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 30 Sep 2026 07:08:54 +0000 Subject: [PATCH 06/12] fix(scan): finish embedded FairScan scan-to-PDF upload Route both the external-app and embedded FairScan scan results through a single field-level ActivityResultLauncher. The upload uses FileUploadHelper directly, instead of routing back to FileDisplayActivity's requestUploadOfContentFromApps() because: 1. The suggested new pattern is ActivityResultLauncher, which didn't go well with the intent-based pattern of FileDisplayActivity. As a reference, AlbumItemsFragment also does its own upload. 2. FileDisplayActivity uses UriUploader which explicitly prevents local files from being uploaded, cf. isSensitiveUri(). But in case of the document scanning, the file is created internally, in the /fairscan folder, and uploaded from there. Removed the now-unused REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME, which was originally introduced for renaming the external FairScan file. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- .../ui/activity/FileDisplayActivity.kt | 14 +--- .../FileDisplayUploadHandler.kt | 17 +---- .../android/ui/fragment/OCFileListFragment.kt | 74 +++++++++++++++++-- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index c1616b00515d..f6c2c60f6097 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -1413,13 +1413,9 @@ class FileDisplayActivity : // region Activity results override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when { - data != null && requestCode in selectContentFromAppsRequestCodes && + data != null && requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS && resultCode in selectContentFromAppsResultCodes -> - uploadHandler.uploadContentFromApps( - data, - resultCode, - autoRename = requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME - ) + uploadHandler.uploadContentFromApps(data, resultCode) data != null && requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && resultCode in selectFilesFromFileSystemResultCodes -> @@ -2635,12 +2631,6 @@ class FileDisplayActivity : val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA = REQUEST_CODE__LAST_SHARED + 6 - val REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME = REQUEST_CODE__LAST_SHARED + 7 - - private val selectContentFromAppsRequestCodes = setOf( - REQUEST_CODE__SELECT_CONTENT_FROM_APPS, - REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME - ) private val selectContentFromAppsResultCodes = setOf(RESULT_OK, UploadFilesActivity.RESULT_OK_AND_MOVE) private val selectFilesFromFileSystemResultCodes = setOf( RESULT_OK, diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt index 2eeff506e423..d82b7e36999a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt @@ -7,9 +7,7 @@ package com.owncloud.android.ui.activity.filedisplayactivity import android.content.Intent -import android.net.Uri import android.os.Parcelable -import androidx.core.util.Function import androidx.lifecycle.lifecycleScope import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -28,11 +26,9 @@ import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailable import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.ui.helpers.UriUploader import com.owncloud.android.utils.StringUtils -import com.owncloud.android.utils.UriUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.apache.commons.io.FilenameUtils import java.io.File class FileDisplayUploadHandler( @@ -42,7 +38,7 @@ class FileDisplayUploadHandler( ) { // region Content from other apps - fun uploadContentFromApps(contentIntent: Intent, resultCode: Int, autoRename: Boolean) { + fun uploadContentFromApps(contentIntent: Intent, resultCode: Int) { val user = activity.user.orElse(null) ?: return val clipData = contentIntent.clipData @@ -59,7 +55,6 @@ class FileDisplayUploadHandler( } val remotePath = activity.getCurrentDir()?.remotePath ?: OCFile.ROOT_PATH - val displayNameTransformer = if (autoRename) Function { timestampedFileName(it) } else null UriUploader( activity, @@ -68,17 +63,9 @@ class FileDisplayUploadHandler( user, behaviour, showWaitingDialog = false, - copyTmpTaskListener = null, - fileDisplayNameTransformer = displayNameTransformer + copyTmpTaskListener = null ).uploadUris() } - - private fun timestampedFileName(uri: Uri): String? { - val displayName = UriUtils.getDisplayNameForUri(uri, activity.applicationContext) - if (displayName.isNullOrEmpty()) return null - - return FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) - } // endregion // region Camera diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 3ca18dbd3734..fcfaa4949c9b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -19,8 +19,10 @@ package com.owncloud.android.ui.fragment import android.Manifest import android.annotation.SuppressLint import android.app.Activity +import android.content.ContentResolver import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper @@ -29,6 +31,7 @@ import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.IdRes import androidx.annotation.OptIn import androidx.annotation.StringRes @@ -43,7 +46,10 @@ import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation import com.nextcloud.client.account.User import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.utils.Throttler +import com.nextcloud.model.OCUploadLocalPathData import com.nextcloud.repository.ClientRepository import com.nextcloud.ui.fileactions.FileAction import com.nextcloud.ui.fileactions.FileActionHandler @@ -106,6 +112,7 @@ import com.owncloud.android.ui.events.SearchEvent import com.owncloud.android.ui.fragment.FileFragment.ContainerActivity import com.owncloud.android.ui.fragment.helper.ParentFolderFinder import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.ui.helpers.UriUploader import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface import com.owncloud.android.ui.navigation.model.ActionBarStyle import com.owncloud.android.utils.FileSortOrder @@ -212,6 +219,9 @@ open class OCFileListFragment : private var fabMain: FloatingActionButton? = null private val multiChoiceModeListener by lazy { OCFileListMultiChoiceModeListener(this) } private val mainHandler = Handler(Looper.getMainLooper()) + private val scanLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + handleScanResult(result.resultCode, result.data) + } private val activeActionMode: ActionMode? get() = multiChoiceModeListener.activeActionMode @@ -569,22 +579,71 @@ open class OCFileListFragment : builder.show() } - @Suppress("DEPRECATION") override fun scanDocUploadFromApp() { val activity = requireActivity() // A separately installed FairScan wins over the built-in scanner, so users can pick up // a newer FairScan release before Nextcloud itself updates the bundled version. - val scanIntent = if (scanIntentExternalApp.resolveActivity(activity.packageManager) != null) { - scanIntentExternalApp + val isSeparateAppInstalled = scanIntentExternalApp.resolveActivity(activity.packageManager) != null + val scanIntent = if (isSeparateAppInstalled) scanIntentExternalApp else FairScan.scanToPdfIntent(activity, true) + + scanLauncher.launch(scanIntent) + } + + private fun handleScanResult(resultCode: Int, data: Intent?) { + val pdfUri = FairScan.pdfUriFromResult(resultCode, data) ?: return + uploadScannedPdf(pdfUri) + } + + private fun uploadScannedPdf(pdfUri: Uri) { + val fileActivity = activity as? FileActivity + if (fileActivity == null) { + Log_OC.e(TAG, "Activity is null, cant upload scanned document") + return + } + + val user = fileActivity.user.getOrNull() + if (user == null) { + Log_OC.e(TAG, "User not exist, cant upload scanned document") + return + } + + val remotePath = currentFile?.remotePath ?: OCFile.ROOT_PATH + + if (pdfUri.scheme == ContentResolver.SCHEME_FILE) { + uploadEmbeddedScannedPdf(user, pdfUri, remotePath) } else { - FairScan.scanToPdfIntent(activity) + uploadExternalScannedPdf(fileActivity, user, pdfUri, remotePath) + } + } + + private fun uploadEmbeddedScannedPdf(user: User, pdfUri: Uri, remotePath: String) { + val localPath = pdfUri.path + if (localPath == null) { + Log_OC.e(TAG, "Scanned document has no local path, cant upload: $pdfUri") + return } - activity.startActivityForResult( - scanIntent, - FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME + val remoteFilePath = remotePath + FileOperationsHelper.getTimestampedFileName(PDF_EXTENSION) + val data = OCUploadLocalPathData.forFile( + user, + arrayOf(localPath), + arrayOf(remoteFilePath), + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE ) + FileUploadHelper.instance().uploadNewFiles(data) + } + + private fun uploadExternalScannedPdf(fileActivity: FileActivity, user: User, pdfUri: Uri, remotePath: String) { + UriUploader( + fileActivity, + listOf(pdfUri), + remotePath, + user, + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + showWaitingDialog = false, + copyTmpTaskListener = null + ).uploadUris() } override fun uploadFiles() { @@ -1512,6 +1571,7 @@ open class OCFileListFragment : private const val ALL_MIME_TYPES = "*/*" private const val SCAN_TO_PDF_ACTION = "org.fairscan.app.action.SCAN_TO_PDF" + private const val PDF_EXTENSION = ".pdf" private const val ACTIVITY_TAB = 0 private const val SHARING_TAB = 1 From 62dc5337c3f0e135f9bcedd6e03b11345937ff38 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 14 Oct 2026 19:12:41 +0200 Subject: [PATCH 07/12] Manually added key for dependency sh.calvin.reorderable Running from gradle somehow sent it into an endless loop. This key was fetched from https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&search=0xAB07D8E9F243E3F6F933B4FAE886D43F9B6F84F9 and added Signed-off-by: Philipp Hasper --- gradle/verification-keyring.keys | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index aa7c1fa58232..4b82834f55c8 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -9424,3 +9424,18 @@ byp2Ws27l1oNhMexztBivHz1OLxGJY8odrCtuxK3JMllDdln/HHdwrp6h7SDRdxR 6llX74zIdctZVsii8eJHvA== =DZ0u -----END PGP PUBLIC KEY BLOCK----- +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: Hockeypuck 2.2 +Comment: + +xjMEZVrPUxYJKwYBBAHaRw8BAQdAFtim0TLdBCDZca6SP9cArWjKwSo5PxZR1LcS +HzNgwZHNG0NhbHZpbiBMaWFuZyA8bWVAY2FsdmluLnNoPsKZBBMWCgBBFiEEqwfY +6fJD4/b5M7T66IbUP5tvhPkFAmVaz1MCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsC +BBYCAwECHgcCF4AACgkQ6IbUP5tvhPnMDwEA8DW39bCnb02L310LRWMgXPen0Jyg +YoIyFUw3s+sKEHwBAM9s+o5zZhXdJoZuzuZj9IEwKK7xsf+ieXJ8OEEJ3vgBzjgE +ZVrPUxIKKwYBBAGXVQEFAQEHQCf5jHOcZ2XPQQZjqpDy3I3hg50eC42rlJp6rAh6 +l5ByAwEIB8J+BBgWCgAmFiEEqwfY6fJD4/b5M7T66IbUP5tvhPkFAmVaz1MCGwwF +CQWjmoAACgkQ6IbUP5tvhPnmlQD/cYdzuob+kcFd0Og9T4VZ0eA/T7vZOUCnC5dQ +UgoqnwUA/inddpH+5Rxs2+xUJ+lZVASRWiFj9XwRDRFG+BFmAocM +=7RNZ +-----END PGP PUBLIC KEY BLOCK----- From af8e8a6b3c8c26980848467ab4ccacfaccb05c7e Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 14 Oct 2026 19:58:48 +0200 Subject: [PATCH 08/12] Updated dependency verification data Ran `./gradlew --write-verification-metadata pgp,sha256 --export-keys help` Signed-off-by: Philipp Hasper --- gradle/verification-keyring.keys | 152 ++++++++++++++++++++++++++++--- gradle/verification-metadata.xml | 98 ++++++++++++++++++++ 2 files changed, 235 insertions(+), 15 deletions(-) diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index 4b82834f55c8..641bda2615cb 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -2138,6 +2138,51 @@ u8veAZEXwuJGYHE= =gx4v -----END PGP PUBLIC KEY BLOCK----- +pub C0E2EB10E7B60FAD +uid Mike Penz (OpenSource library signing) + +sub 488E1BD9D30006C0 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsFNBF4Vpt4BEACv9ZDb6xRuHRJTVJuDRG+A0sjAZJjUy6NUOEtVNUqGZYT0NNqa +fNLXw/79Y0RmmG1Q9v+vmE242IyO5AamML/Lz57jO6rQp3Wmg0HH6TofMV5iKGNX +dlt2uYi5eWrzoiE5hG3RlP+7WV7lPqoUHx2ES20ilIQxOG307mnNJmy0NzupBHV9 +B7vMPgo2XYnG3Q/B/9n9IO/Uk2m9ejSUWMqe6gWjLmhVdl8TgXWCqhhMach/MU6q +62nOUvbp5VzHks2d9CyRSALMlc2Pj8fdBREt3pTmMW5SbnrVFcQBd6NojuBjD/Az +//MXy6JjAVbtu2YoN82TAPhOmj2dAHbOrdef/i/IaIPPdokpMogHZVuu0TLPWn71 +d7IJtw1eVLAgj2B5ydG9fLEmf3oiI4T9OSSLq94f4ijOQH3n0hx/JO14va1Qq+jJ +JK7nIMkusFpsDAhV6oKeYHu9L4FPc408Beuj2ZALBw2ZlItX1uQQugtG3RmQRjr8 +aEUanpptdSHAy8aP9EdyVdOEIiljJVXZWjlcHuRuIZBbCeffnTJpnEkQqEGyndZL +C0XlYEt+Zwjio4opNcdwVZ12TvQq7NbUP69o1NTOXeZapZEZ9n8o2wiaX4PhFgry +1XX4aCRNXAmdyBQvZ0NkzmEx4pK26tNozNZyVm5882pbZtGgum3mrsa5SwARAQAB +tDtNaWtlIFBlbnogKE9wZW5Tb3VyY2UgbGlicmFyeSBzaWduaW5nKSA8bWlrZXBl +bnpAZ21haWwuY29tPs7BTQReFabeARAAuMzYa/C7VQLdR9mVE/n0kFiywX8J4dCt +vrpjyi/JU7sGZWTEAHoq/DuYAXn9eFbyzpmVRuezr1jfYkKGINxRTqowuG0mHOKJ +snzkB64bObYVaAQJuMQozNswrLofzmcuvPC9fTHMXsQypMnaF6mFzGOwRGpkoCP/ +BSBPn73mSkDcLCRAZZrdPNkQoICtRpNvZ2uMEvjhIGZd0HzT1bGd2FDbNIdGT3N9 +2s813rjRmvPk9xwCzqDFQDliwjHhGsP7vOcz1EJue9px5hFz7AQTNZOqVhHIucW4 +vDpKlpBSE2ofl0DMt9QhdAKd9TWvdogaaryTBO2CwBqoXtgSTcyf17yEP2q4OCwf +9oOELTJZaPGDnSOb1ooH2arwrSRoblxZhtnswq+i+Y7/JKoY5u5HAKb7ARzg6GEL +jWxMKgcHoaWjYsqxafzSrwlaAc6sy8pPthjSKPkDtMkPIvtXZMJhnpwWoJWvJN3y +xT+TwGex99MXOsoZvgGjJ6pgSOEpRtDhuaEmqi39h+uETZHiBQBhXOmRJyNr2XB3 +LS2fsHVReUzv0VLktkGsHxibeVmn8PeJf+vg2USUekBXtRSb2V4uTGp79eSvghns +ME3YfEFVU/iNRc9/oXQvFqT8kR6VN0TtpwR73HpeszmsYO2Em0gH21H5VZhVKFYM +bepll0I/JVEAEQEAAcLBfAQYAQoAJhYhBOBPygdscW2mdzVl6sDi6xDntg+tBQJe +FabeAhsMBQmWg7CAAAoJEMDi6xDntg+tfqsP/iaW5Z4u20A8gKr/E/MGxqrag1VR +J339PTaLc91tovE8YVOzHWutEv324NVHzL0DLaeBUbqZx+m3w2AyE3FxHLjdtXyz +w/n1VWJbmi4xhozSxJVbUZ9Uzt3W3E6DXwa9Pgsrx9RTPy+l8rU+sMFY5dMxDDGd +odVBCdmJcgeKtVSs5cv3pN5YUvtH1UOCLc1ohpEC32wOrhJWZM89H9/R3jUds9c0 +1kOpoW5cdv81hguLUCjr+bO9Dc/0LJQI5b1HnZd3xbDDtxtlMTVW6dz+58DCpnD8 +r0D/Yc0Z/N+ngRX4wzSdEdjCf7atY8kIcRyFyQDoHSrAFxLbEvCESjfzSMBVAtGD +RYFlPnmR0kfCaChmn0k4Goxce7wDtox4fkQMO8Lx/+DDxNhVEGTO7gDKjmz0lfOA +rKLghBhAmUK493D5T0GsjgF3dV4FTuieVTCCmaE2mc4qInELVZ5GAn8Aas6dj63D +88JdIES6TE6eBr5FShvJZIHxRl7Uzsx0N5eO89Et/5TWJ5zsPe3U6MkHY5yhFztM +ijiQ6PVAbD/I0J7Y+cDyOxBvKA9+DyvVGwGlgU4EU2MZVy21G+vwoTISlqEUB9+D +AguFGb+9+c3FVUpU7QAupW8fU4tAkUZbiZkGHXN7wADhiSCilEk8O1gTmNASaUcz +e31IKwLC/NOHo4/A +=SROI +-----END PGP PUBLIC KEY BLOCK----- + pub C2B98E64C61147AC sub 23B5E29FACD5C2B5 -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -2954,6 +2999,50 @@ tJscrv0/jtIwo5zy8SqldnhB6g== =OReJ -----END PGP PUBLIC KEY BLOCK----- +pub D7662013FA1A5596 +uid skydoves (Jaewoong Eum) + +sub CBC29FFF4999D93A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsFNBFzX5PUBEACaGSIYimcJyfAqK85lUc2cxCG8gnT80AfnEpNtoxixRsYGgwmm +MVAOXBb9CLbiMNV++AYRLuANXoqbHbF6yLRf4qJyXHOrnQltiA6WYxyDupVPFa4K +OF/gFskG8T2S+pY6YPXxt66w3lLR4hh9zuJCcmJafdzL9Lg/h7yFQzSXp9B6qgI2 +MAl3tPwlVxiTRFX8uMfp+t40skv3XkjCHEVj6qah1VJyiwqmd+sl52XaZ+3/URkw +KjOShP33tFK5Z9E1vI8fPHO7Bh6uR+/Pho40TQp7hBCK/EIbMJAJ72zJEVUiR3RW +tWoTkovrFxDsqjPOAxf2pLkwG92wwlYhJlrwN8ViDrGGwMGeR7vfQC0xqkU8OxSF +m/x4XW1lRAOa2gExUF5bDNlFWTuTfDXn9If4yin7NB0fMvyM2H0LWkS8hL9b/j8J +DkgtEmYrXqhZYfCsmM2foiCqm66sJatN5G1ul1yGzV/v3TU2wGJP4L6fjdBc6xdE +DfG/nfvng2R7m5PMA1lk+q2wODLff9yHb72jYhKBmpSIPGlyCKuLdrNTj8+V7Brx +sLBLx26vy046+fOnED2mnU6hnQByZhM19zEZ3GQR6w+rivPH7ewjz/Qqmm1Vv04z +ZbgN0JfGTQEwVitNP48P8OVsOwyN4i51vXhBv5tu+Q0KS3dk3Uvv1JwyHwARAQAB +tCxza3lkb3ZlcyAoSmFld29vbmcgRXVtKSA8c2t5ZG92ZXNAbmF2ZXIuY29tPs7B +TQRc1+T1ARAAvZXWe7ebrisRHaTVHi24UMD+bN9dI5TCqVN+WlaEoaFDk6/g/h7n +Aya1uKTBPftItv5mJiPsVye2JG8GD+yklh9a5wz3GOVt4lbXmHh+T/NFsAyT0k1n +8qpSdo3F0gJlL6D/2uTNTCvHzs+xBzGkNU4dBoHJPj83Ubu/Saoaz7rc8Aqe5s3y +jlurEfEKXQ2CRl6X/80+QnGpYmfwiPrGGiz17mMU+9DY0kDcPOtmeVL5fIqmVZZr +Ha6nC7teXWN2IZQroPXM6esyDZ7UCRtuGH74vPeOf/bfGwAgz7VvlWD4qTBd2qLa +tMEpSv+zwM064+DDswXuMMK2PabHOjxZd/GHB98jSF91WyWdSTajzkcm5pF6Gd99 +69st+uOUzwCYiyukE/XMpWLzfF+FtTty75k57Edrun+ykr9vIEyRpgo1TICnstVk +NLNt22xayw9RCXz5Uv8dhf7xLi0mLGFWoW8JRub8ChKKxX1AO9seFtp0KKPy5CCg +dntIF5/cKIG2c8BDlNXE6hZUI2a3c9a0gtMntC53YAtNEn+2REIANadhOvhbdYvK +cu0zvXlSo1DgSEjNwYRBGwjMOKgUZ4qer71XI1LnyKu+uwLrBs10tIH76Pr//Tuz +cZ8cxayx6SPFfBd8LrdOz3rwimkbG/UNTMh9BsF9V1OK6eKd4nVaI5cAEQEAAcLB +XwQYAQIACQUCXNfk9QIbDAAKCRDXZiAT+hpVls1GD/4hBAZbjrMkGIg5MipHI/h/ +n2LQPu8fhd9d3N2oFLFXn/0iXFesaob9O5QprOMP70EEOJN6WsC3OlqrlposNKES +kNXp9mhIGn8yG4oLL44hwNJQP9brdVqumsh3ehUCaLJQfbUE3xGx2XPwpgxnULPx +Q7RXdw73/JSoJqa8j0/X0TmdQQsJVeJ//cWOKlchdGh1aPjma8tJKELYNCpNZkAI +tH8lYaEg7bistSndJZas136ECaIujk2Wt1CJLKdsdXWn9UOiLvA38HbRKAtAbnmi +VdzvwJQDKbqGwWzfH+bAjgGto8MFM3+nmrqQ6JhYISQG7nWFQAgDiCX5opw1GWFR ++gFy6vzoIAcU8jt/F4n7erxDuQiDRcVLxPAwBI79IlsL4qHBuU95OQPDxFkHt+hC +WLxvZ96HZczlYmpNMTXjum8j8t0/EeVyzYQAVTjheAlSmbPa/zB8MpckwLc2kO3h +fBiNJDfXoptaZ4H6DFxNDHf9sD+d31nWBrJoRti7a8IQiC7QswSFdYbL5RH9huuo +wOTnyt/xBwGRMvYkz7pwGvO8wya3KaBYefL2/UUcsK1dVJm9H6DAQ+sCKaDBaO1n +i27UpDdLHUHr4irAPZs+6qWHOYVFFy8uBLah7rUvV69Lpt5JIPLaiS7M3kgig2YG +MD9W3Ckbgcahr/9YO8OFLg== +=I2kh +-----END PGP PUBLIC KEY BLOCK----- + pub D7C92B70FA1C814D uid Matthew Sicker (Signing Key) @@ -3324,6 +3413,39 @@ u4awI7kDfdqz1V5s5prLGlkoPhACUxRZeznT3Js7NVE= =ZRYf -----END PGP PUBLIC KEY BLOCK----- +pub E0F9DC177239CD8A +sub 12F64A1C91895516 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBGWNTxIBDADmE/W3AVPhGWIHBGoXk4gwN0htHmclNqfoL5ghNS0GrMm6B99J +tRExRf45/cqo6pfWgZmPoxB/6BUBU96hb1pAdOPGjq4AyDgDwg9W/yvTPTRvnsds +SmIE35YlYN8LDXVgWAKEVOmNFOgFw+xDcDvKCUTrBq02cSfMMh8jfrbylCT2U0bD +hDqlOEJ0pTUmOsnNr0+6ds4FHCJneNR4jQKkI5g8qAALPKNRr66KTR5qckeL90xi +KMtbgzd4nBClfMfxrV7Ysw/xmriKqLK8q0A9A1xQwK5JYEIab6viVIvUD3IlWE0T +HLoPQQZGuYzICI4UHBQWN/aRA1HWruAL8ms4I+XS63r8LiDDyYgXlzDr0ZngB82S +RUpnJX9UEIZt6R0mURLJ8EoUfnP/wpb7rNeepuyvXDwQ9EaGXEUoI7ldJjWUeVpw ++zoPHFieXG2LjBzrhI5sIzd6FSt1UbvsAQnkuIZTGhDAlI4SkeyArFLgq1vsaRwv +bYoUpoyPyf2+158AEQEAAc7AzQRljU8SAQwAtmgIFYFPuTziWJvkrocnER2eSRun +Zp5DCxb+aGolVS521bebxcTXv+jcgpuzJGCXsU4yv8jQY2tgwXq+13nPw1GJdPn0 +TiqXNZRgY5/cwVVl7haawUXoXt8F/alH3ruyy4AUKyZIk1fO6Ald/LMEhDJKCr0z +s0YLqk8d1gG7y1Y9ORwwa1DIL+Y+NugmKOqzCLhOQ4ojvMHQ1xq/EgP91pgErznV +k65jx0+vKcrdkZiPQfZDIvDeXG/mu8UB5IzjUXbShG9T07VGmd87q9I8ZbGCwOR0 +GT3bs5sdX/IMUQs7HZUOERum5eDJ/e5bo9iWBY9ffGISKJQQ5jZsS1rwr2ioRjO3 +vfiWrT+0u12sxdGDxuaoWZkqmqcssmSCSiAKaWEtba6IZdm0V1ZuyUbODp9+8DqC +JsnMmMaGXC6GlkEdHW/0DkdqnRANRZ6UhU0q+pJExlc5rQichgpm0mk77HRsCDYN +Ga9ZUj/nJwJLotJFlRXVIIwljZ+wJfyWgRdvABEBAAHCwPwEGAEKACYWIQTtqFiD +iTwsoJgdWlTg+dwXcjnNigUCZY1PEgIbDAUJA8JnAAAKCRDg+dwXcjnNinhbC/9I +oYxAERxkBllxY5FaIWvVn35v3axQRCvqz8v1lgPL2LrIkokcmyZENbMoIaC25E8B +JKgQjJtLJKJzE5sOLQV/rYnHUSVwTn+mm8iyNu3VhRtfoh3uq4S+ZmFixs76xFla +em5tO9h/7UBH29OmDuYm3eQAZF/ASmXkvRuEOx3WtD6YFNUQ3V61t46/1PlIoZ9x +EA6WkrHL8z4+HEaL+ftCVGwL+F8q2Hs5UI9C/dnESYwenYOmMRssgSqp3+8T+00x +rWdGY/mNAUCTlSoAjsE5vFMz3dJZKMdpuuIrZMnTMh1Em0wa9+9yz8dzOicp02ld +M9fgmoXl4za2RZM6WtR74KYfroAu5YZm9sNkCPmT/fqJYotA2OJsq7e3x5PQKVP1 +BoYmenmPgRcrfZVJALQPKiunh9l5VYEq7DgTJ+520XG7brQKKxnp8wpIkXVcWSqz +ksd/LwATBPYf7rfKuxEWDfCy294FXhfUSAIpYlHvMSNvxqots45yDwNfSWpwJ/Y= +=nPBW +-----END PGP PUBLIC KEY BLOCK----- + pub E126833F9CF64915 sub C64121F5021754AB -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -3592,6 +3714,21 @@ kDPpOUpVEh9FB0MrCNxY+0pa =w5DF -----END PGP PUBLIC KEY BLOCK----- +pub E886D43F9B6F84F9 +uid Calvin Liang + +sub 70B51E4211514239 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEZVrPUxYJKwYBBAHaRw8BAQdAFtim0TLdBCDZca6SP9cArWjKwSo5PxZR1LcS +HzNgwZG0G0NhbHZpbiBMaWFuZyA8bWVAY2FsdmluLnNoPs44BGVaz1MSCisGAQQB +l1UBBQEBB0An+YxznGdlz0EGY6qQ8tyN4YOdHguNq5SaeqwIepeQcgMBCAfCfgQY +FgoAJhYhBKsH2OnyQ+P2+TO0+uiG1D+bb4T5BQJlWs9TAhsMBQkFo5qAAAoJEOiG +1D+bb4T55pUA/3GHc7qG/pHBXdDoPU+FWdHgP0+72TlApwuXUFIKKp8FAP4p3XaR +/uUcbNvsVCfpWVQEkVohY/V8EQ0RRvgRZgKHDA== +=vhh3 +-----END PGP PUBLIC KEY BLOCK----- + pub E93671C7272B7B3F uid Rolf Lear (JDOM) (Used to sign JDOM Packages) @@ -9424,18 +9561,3 @@ byp2Ws27l1oNhMexztBivHz1OLxGJY8odrCtuxK3JMllDdln/HHdwrp6h7SDRdxR 6llX74zIdctZVsii8eJHvA== =DZ0u -----END PGP PUBLIC KEY BLOCK----- ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: Hockeypuck 2.2 -Comment: - -xjMEZVrPUxYJKwYBBAHaRw8BAQdAFtim0TLdBCDZca6SP9cArWjKwSo5PxZR1LcS -HzNgwZHNG0NhbHZpbiBMaWFuZyA8bWVAY2FsdmluLnNoPsKZBBMWCgBBFiEEqwfY -6fJD4/b5M7T66IbUP5tvhPkFAmVaz1MCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsC -BBYCAwECHgcCF4AACgkQ6IbUP5tvhPnMDwEA8DW39bCnb02L310LRWMgXPen0Jyg -YoIyFUw3s+sKEHwBAM9s+o5zZhXdJoZuzuZj9IEwKK7xsf+ieXJ8OEEJ3vgBzjgE -ZVrPUxIKKwYBBAGXVQEFAQEHQCf5jHOcZ2XPQQZjqpDy3I3hg50eC42rlJp6rAh6 -l5ByAwEIB8J+BBgWCgAmFiEEqwfY6fJD4/b5M7T66IbUP5tvhPkFAmVaz1MCGwwF -CQWjmoAACgkQ6IbUP5tvhPnmlQD/cYdzuob+kcFd0Og9T4VZ0eA/T7vZOUCnC5dQ -UgoqnwUA/inddpH+5Rxs2+xUJ+lZVASRWiFj9XwRDRFG+BFmAocM -=7RNZ ------END PGP PUBLIC KEY BLOCK----- diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8816c7adae60..cc09655079ab 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -22,6 +22,8 @@ + + @@ -55,12 +57,14 @@ + + @@ -86,6 +90,7 @@ + @@ -93,6 +98,7 @@ + @@ -108,6 +114,7 @@ + @@ -384,6 +391,7 @@ + @@ -458,6 +466,7 @@ + @@ -551,6 +560,7 @@ + @@ -563,6 +573,7 @@ + @@ -1656,6 +1667,11 @@ + + + + + @@ -7259,6 +7275,11 @@ + + + + + @@ -25377,6 +25398,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25726,6 +25787,14 @@ + + + + + + + + @@ -30040,6 +30109,14 @@ + + + + + + + + @@ -30565,6 +30642,14 @@ + + + + + + + + @@ -33657,6 +33742,19 @@ + + + + + + + + + + + + + From 81d2731a34433a9d6bcad8042b0b02892ffe6ac8 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 18 Oct 2026 15:42:54 +0200 Subject: [PATCH 09/12] feat(scan): Using library fork of FairScan, available on Jitpack Assisted-by: ClaudeCode:claude-sonnet-5.5 Signed-off-by: Philipp Hasper --- .../action.yml | 44 ------------------- .github/workflows/analysis.yml | 1 - .github/workflows/assembleFlavors.yml | 1 - .github/workflows/check.yml | 1 - .github/workflows/detectWrongSettings.yml | 1 - .github/workflows/garm.yml | 2 - .github/workflows/qa.yml | 4 -- .github/workflows/unit-tests.yml | 2 - gradle/libs.versions.toml | 5 +-- settings.gradle.kts | 11 ----- 10 files changed, 2 insertions(+), 70 deletions(-) delete mode 100644 .github/actions/publish-fairscan-to-maven-local/action.yml diff --git a/.github/actions/publish-fairscan-to-maven-local/action.yml b/.github/actions/publish-fairscan-to-maven-local/action.yml deleted file mode 100644 index a95492172f3b..000000000000 --- a/.github/actions/publish-fairscan-to-maven-local/action.yml +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - -name: "Publish FairScan to mavenLocal" -description: > - Checks out FairScan and publishes it to mavenLocal so a subsequent Gradle build of :app in the - SAME job can resolve org.fairscan:app from mavenLocal(), as configured in settings.gradle.kts. - Must run in the same job as the nextcloud-android build (writes to ~/.m2/repository on the - runner's filesystem) and after a JDK 17-21 has already been set up by the calling workflow - (FairScan's Gradle 8.13 wrapper needs JDK 17-21; JDK 25 fails). - -inputs: - repository: - description: "FairScan repository to check out (owner/repo)." - required: false - # TEMPORARY: points at a fork+branch, not a tagged FairScan release. FairScan isn't published - # anywhere (no JitPack, no release with the new library shape) yet, so CI builds it from - # source. Update this default (and settings.gradle.kts' mavenLocal repository, and - # gradle/libs.versions.toml's fairscanVersion) once FairScan publishes a real, versioned release. - default: "phillab/FairScan" - ref: - description: "FairScan ref to build. See the 'repository' input comment: temporary." - required: false - default: "ph/offer_as_library" - path: - description: "Directory (relative to the job workspace) to check FairScan out into." - required: false - default: "fairscan-src" - -runs: - using: "composite" - steps: - - name: Checkout FairScan - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: ${{ inputs.repository }} - ref: ${{ inputs.ref }} - path: ${{ inputs.path }} - persist-credentials: false - - - name: Publish FairScan to mavenLocal - shell: bash - working-directory: ${{ inputs.path }} - run: ./gradlew --no-daemon publishToMavenLocal diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index c7bed201cffa..067a8a663cd0 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -61,7 +61,6 @@ jobs: with: distribution: "temurin" java-version: 25 - - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Install dependencies run: | sudo apt install python3-defusedxml diff --git a/.github/workflows/assembleFlavors.yml b/.github/workflows/assembleFlavors.yml index 1067b63175a2..3c77066448d7 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -40,7 +40,6 @@ jobs: - uses: gradle/actions/wrapper-validation@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - name: Setup Gradle uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Build ${{ matrix.flavor }} run: | { diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a0a697c90a7c..212ec4adef78 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -32,6 +32,5 @@ jobs: java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Check ${{ matrix.task }} run: ./gradlew ${{ matrix.task }} diff --git a/.github/workflows/detectWrongSettings.yml b/.github/workflows/detectWrongSettings.yml index 6c1073e112c7..09d2cdfb0f14 100644 --- a/.github/workflows/detectWrongSettings.yml +++ b/.github/workflows/detectWrongSettings.yml @@ -26,6 +26,5 @@ jobs: with: distribution: "temurin" java-version: 21 - - uses: ./.github/actions/publish-fairscan-to-maven-local - name: Detect SNAPSHOT run: scripts/analysis/detectWrongSettings.sh diff --git a/.github/workflows/garm.yml b/.github/workflows/garm.yml index 85b513387ea1..c974bbaa257c 100644 --- a/.github/workflows/garm.yml +++ b/.github/workflows/garm.yml @@ -50,8 +50,6 @@ jobs: distribution: "temurin" java-version: 21 - - uses: ./.github/actions/publish-fairscan-to-maven-local - - name: Configure gradle daemon run: | mkdir -p $HOME/.gradle diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 13ac39ecce3a..e6b5696cda99 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -37,10 +37,6 @@ jobs: distribution: "temurin" java-version: 25 - - name: Publish FairScan to mavenLocal - if: ${{ steps.check-secrets.outputs.ok == 'true' }} - uses: ./.github/actions/publish-fairscan-to-maven-local - - name: Build QA id: build-qa if: ${{ steps.check-secrets.outputs.ok == 'true' }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 5be789e1f7c5..2129695b32c3 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -39,8 +39,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - - uses: ./.github/actions/publish-fairscan-to-maven-local - - name: Run unit tests with coverage run: ./gradlew jacocoTestGplayDebugUnitTest diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d3dadaafd9c8..7cc27a1fbfdc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,8 +8,6 @@ androidImageCropperVersion = "4.7.0" androidLibraryVersion ="0c7fb895b2b621d998e8a32faec2db9ca046c1ef" androidOpensslVersion = "3.5.6.1" androidPluginVersion = "9.5.0" -# Temporarily using the "-SNAPSHOT" suffix for dev testing via to mavenLocal. Remove once published online -fairscanVersion = "2.3.0-SNAPSHOT" androidsvgVersion = "1.4" androidxTestVersion = "1.7.0" annotationVersion = "1.11.0" @@ -33,6 +31,7 @@ espressoVersion = "3.7.0" eventbusVersion = "3.3.1" exifinterfaceVersion = "1.4.2" ezVcardVersion = "0.12.2" +fairscanVersion = "v2.3.0-11-gc1517c5-lib-1" fbContribVersion = "7.7.4" findsecbugsPluginVersion = "1.14.0" firebaseMessagingVersion = "26.0.1" @@ -128,7 +127,7 @@ ext-strikethrough = { module = "io.noties.markwon:ext-strikethrough", version.re ext-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwonVersion" } ext-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref = "markwonVersion" } ez-vcard = { module = "com.googlecode.ez-vcard:ez-vcard", version.ref = "ezVcardVersion" } -fairscan = { module = "org.fairscan:app", version.ref = "fairscanVersion" } +fairscan = { module = "com.github.phillab.fairscan-library-fork:scan-ui", version.ref = "fairscanVersion" } findsecbugs-plugin = { module = "com.h3xstream.findsecbugs:findsecbugs-plugin", version.ref = "findsecbugsPluginVersion" } fb-contrib = { module = "com.mebigfatguy.fb-contrib:fb-contrib", version.ref = "fbContribVersion" } firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseMessagingVersion" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 63a75a62ed10..80f22edeedaf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,17 +38,6 @@ dependencyResolutionManagement { } mavenCentral() maven("https://jitpack.io") - // FairScan isn't published to JitPack yet. Until then, it's consumed from mavenLocal, - // published there manually and explicitly, same as any other binary dependency you're - // developing against locally before it has a real release: - // ./gradlew publishToMavenLocal - // Re-run that whenever the FairScan checkout is updated to a different commit/tag. Once - // FairScan publishes to JitPack for real, this mavenLocal entry goes away and the - // coordinate in gradle/libs.versions.toml switches to the JitPack one - nothing else - // about how :app depends on it changes. - mavenLocal { - content { includeGroup("org.fairscan") } - } } } From a481787ba5b0c8dd5f4a6d2a8aa79bcdd04c3a81 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Tue, 20 Oct 2026 09:14:35 +0200 Subject: [PATCH 10/12] Added verification metadata for fairscan library Jitpack artifact 1. Ran `gradlew --write-verification-metadata pgp,sha256 --export-keys help` and compared the hashes for the .jar and .aar with those built locally. They match. 2. The .module files are not binary equivalent, as the build metdadata and the JSON format is slightly different. The hash doesn't match, but inspecting them, they look semantically equivalent. Signed-off-by: Philipp Hasper --- gradle/verification-metadata.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index cc09655079ab..f867034102e4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -38,6 +38,7 @@ + @@ -24987,6 +24988,22 @@ + + + + + + + + + + + + + + + + From 29f74aa7493ac5445b2d8f5ae6d03481e817ef51 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 21 Oct 2026 08:42:05 +0200 Subject: [PATCH 11/12] test(scan): Made external/internal distinction unit-testable Assisted-by: ClaudeCode:claude-sonnet-5.5 Signed-off-by: Philipp Hasper --- .../nextcloud/client/scan/ScannedPdfUpload.kt | 30 ++++++++++ .../android/ui/fragment/OCFileListFragment.kt | 30 ++++------ .../client/scan/ScannedPdfUploadTest.kt | 55 +++++++++++++++++++ 3 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt create mode 100644 app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt diff --git a/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt b/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt new file mode 100644 index 000000000000..25886cb72f17 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.scan + +import android.content.ContentResolver +import android.net.Uri + +sealed interface ScannedPdfUpload { + data class Embedded(val localPath: String, val remoteFilePath: String) : ScannedPdfUpload + + data class External(val uri: Uri, val remoteFolderPath: String) : ScannedPdfUpload + + data object Invalid : ScannedPdfUpload + + companion object { + fun from(pdfUri: Uri, remoteFolderPath: String, remoteFileName: String): ScannedPdfUpload { + if (pdfUri.scheme != ContentResolver.SCHEME_FILE) { + return External(pdfUri, remoteFolderPath) + } + + val localPath = pdfUri.path ?: return Invalid + return Embedded(localPath, remoteFolderPath + remoteFileName) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index fcfaa4949c9b..3413989d81ac 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -19,7 +19,6 @@ package com.owncloud.android.ui.fragment import android.Manifest import android.annotation.SuppressLint import android.app.Activity -import android.content.ContentResolver import android.content.Context import android.content.Intent import android.net.Uri @@ -48,6 +47,7 @@ import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.scan.ScannedPdfUpload import com.nextcloud.client.utils.Throttler import com.nextcloud.model.OCUploadLocalPathData import com.nextcloud.repository.ClientRepository @@ -609,36 +609,30 @@ open class OCFileListFragment : } val remotePath = currentFile?.remotePath ?: OCFile.ROOT_PATH + val fileName = FileOperationsHelper.getTimestampedFileName(PDF_EXTENSION) - if (pdfUri.scheme == ContentResolver.SCHEME_FILE) { - uploadEmbeddedScannedPdf(user, pdfUri, remotePath) - } else { - uploadExternalScannedPdf(fileActivity, user, pdfUri, remotePath) + when (val upload = ScannedPdfUpload.from(pdfUri, remotePath, fileName)) { + is ScannedPdfUpload.Embedded -> uploadEmbeddedScannedPdf(user, upload) + is ScannedPdfUpload.External -> uploadExternalScannedPdf(fileActivity, user, upload) + ScannedPdfUpload.Invalid -> Log_OC.e(TAG, "Scanned document has no local path, cant upload: $pdfUri") } } - private fun uploadEmbeddedScannedPdf(user: User, pdfUri: Uri, remotePath: String) { - val localPath = pdfUri.path - if (localPath == null) { - Log_OC.e(TAG, "Scanned document has no local path, cant upload: $pdfUri") - return - } - - val remoteFilePath = remotePath + FileOperationsHelper.getTimestampedFileName(PDF_EXTENSION) + private fun uploadEmbeddedScannedPdf(user: User, upload: ScannedPdfUpload.Embedded) { val data = OCUploadLocalPathData.forFile( user, - arrayOf(localPath), - arrayOf(remoteFilePath), + arrayOf(upload.localPath), + arrayOf(upload.remoteFilePath), FileUploadWorker.LOCAL_BEHAVIOUR_MOVE ) FileUploadHelper.instance().uploadNewFiles(data) } - private fun uploadExternalScannedPdf(fileActivity: FileActivity, user: User, pdfUri: Uri, remotePath: String) { + private fun uploadExternalScannedPdf(fileActivity: FileActivity, user: User, external: ScannedPdfUpload.External) { UriUploader( fileActivity, - listOf(pdfUri), - remotePath, + listOf(external.uri), + external.remoteFolderPath, user, FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, showWaitingDialog = false, diff --git a/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt b/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt new file mode 100644 index 000000000000..f971012b5d76 --- /dev/null +++ b/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.scan + +import android.net.Uri +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +class ScannedPdfUploadTest { + + private fun uri(scheme: String?, path: String?): Uri = mockk { + every { this@mockk.scheme } returns scheme + every { this@mockk.path } returns path + } + + @Test + fun fileSchemeUriIsTreatedAsEmbeddedScan() { + val result = ScannedPdfUpload.from(uri("file", "/data/files/fairscan/scan.pdf"), "/Documents/", "scan.pdf") + + assertEquals(ScannedPdfUpload.Embedded("/data/files/fairscan/scan.pdf", "/Documents/scan.pdf"), result) + } + + @Test + fun embeddedScanInRootFolderUsesRootPath() { + val result = ScannedPdfUpload.from(uri("file", "/data/scan.pdf"), "/", "20260101_scan.pdf") + + assertEquals(ScannedPdfUpload.Embedded("/data/scan.pdf", "/20260101_scan.pdf"), result) + } + + @Test + fun contentSchemeUriIsTreatedAsExternalScan() { + val uri = uri("content", "/scan.pdf") + + assertEquals(ScannedPdfUpload.External(uri, "/Documents/"), ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf")) + } + + @Test + fun uriWithoutSchemeIsTreatedAsExternalScan() { + val uri = uri(null, null) + + assertEquals(ScannedPdfUpload.External(uri, "/Documents/"), ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf")) + } + + @Test + fun fileSchemeUriWithoutPathIsInvalid() { + assertEquals(ScannedPdfUpload.Invalid, ScannedPdfUpload.from(uri("file", null), "/", "scan.pdf")) + } +} From 1c8d59dc29a9c9f6630d29f1ab39f719fa741b6d Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 21 Oct 2026 22:46:05 +0200 Subject: [PATCH 12/12] test(scan): Added instrumented test Assisted-by: ClaudeCode:claude-sonnet-5.5 Signed-off-by: Philipp Hasper --- .../ui/fragment/OCFileListFragmentScanIT.kt | 115 ++++++++++++++++++ .../nextcloud/client/scan/ScannedPdfUpload.kt | 1 + .../client/scan/ScannedPdfUploadTest.kt | 10 +- 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt new file mode 100644 index 000000000000..db8fdc830b1b --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.os.SystemClock +import androidx.test.core.app.launchActivity +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.test.TestActivity +import com.owncloud.android.AbstractIT +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.nextcloud.client.jobs.upload.FileUploadWorker +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test + +class OCFileListFragmentScanIT : AbstractIT() { + + private lateinit var uploadsStorageManager: UploadsStorageManager + + @Before + fun setUp() { + Intents.init() + uploadsStorageManager = UploadsStorageManager( + UserAccountManagerImpl.fromContext(targetContext), + targetContext.contentResolver + ) + uploadsStorageManager.removeUploads(account.name) + } + + @After + fun tearDown() { + Intents.release() + uploadsStorageManager.removeUploads(account.name) + } + + @Test + fun scanStartsEmbeddedFairScanWithAutoDeletionDisabled() { + scan(Activity.RESULT_CANCELED, null) + + val scanIntent = Intents.getIntents().single { it.action == FAIRSCAN_ACTION } + assertEquals(FAIRSCAN_ACTIVITY, scanIntent.component?.className) + assertEquals(FAIRSCAN_ACTION, scanIntent.action) + assertTrue(scanIntent.getBooleanExtra(FAIRSCAN_EXTRA_DISABLE_AUTO_DELETION, false)) + } + + @Test + fun cancelledScanDoesNotUpload() { + scan(Activity.RESULT_CANCELED, null) + SystemClock.sleep(NO_UPLOAD_GRACE_MILLIS) + + assertEquals(0, uploadsStorageManager.getAllStoredUploads().size) + } + + @Test + fun embeddedScanResultIsQueuedForUploadWithMoveBehaviour() { + val pdf = createFile("scan.pdf", 1) + scan(Activity.RESULT_OK, Intent().putExtra(FAIRSCAN_EXTRA_PDF_PATH, pdf.absolutePath)) + + val upload = waitForUpload() + assertEquals(pdf.absolutePath, upload.localPath) + assertTrue(upload.remotePath.startsWith("/")) + assertTrue(upload.remotePath.endsWith(".pdf")) + assertEquals(FileUploadWorker.LOCAL_BEHAVIOUR_MOVE, upload.localAction) + assertEquals(account.name, upload.accountName) + } + + private fun scan(resultCode: Int, data: Intent?) { + launchActivity().use { scenario -> + intending(hasAction(FAIRSCAN_ACTION)).respondWith(Instrumentation.ActivityResult(resultCode, data)) + scenario.onActivity { activity -> + val fragment = OCFileListFragment() + activity.addFragment(fragment) + activity.supportFragmentManager.executePendingTransactions() + fragment.scanDocUploadFromApp() + } + SystemClock.sleep(RESULT_DELIVERY_MILLIS) + } + } + + private fun waitForUpload(): OCUpload { + val deadline = SystemClock.elapsedRealtime() + WAIT_TIMEOUT_MILLIS + while (SystemClock.elapsedRealtime() < deadline) { + uploadsStorageManager.getAllStoredUploads().firstOrNull()?.let { return it } + SystemClock.sleep(WAIT_POLL_INTERVAL_MILLIS) + } + fail("No upload was queued for the scanned PDF") + error("unreachable") + } + + private companion object { + const val FAIRSCAN_ACTIVITY = "org.fairscan.app.MainActivity" + const val FAIRSCAN_ACTION = "org.fairscan.app.action.SCAN_TO_PDF" + const val FAIRSCAN_EXTRA_DISABLE_AUTO_DELETION = "org.fairscan.app.extra.DISABLE_AUTO_DELETION" + const val FAIRSCAN_EXTRA_PDF_PATH = "org.fairscan.app.extra.PDF_PATH" + const val RESULT_DELIVERY_MILLIS = 1_000L + const val NO_UPLOAD_GRACE_MILLIS = 1_000L + const val WAIT_TIMEOUT_MILLIS = 10_000L + const val WAIT_POLL_INTERVAL_MILLIS = 200L + } +} diff --git a/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt b/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt index 25886cb72f17..e8a9b7fdf6d9 100644 --- a/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt +++ b/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt @@ -18,6 +18,7 @@ sealed interface ScannedPdfUpload { data object Invalid : ScannedPdfUpload companion object { + @Suppress("ReturnCount") fun from(pdfUri: Uri, remoteFolderPath: String, remoteFileName: String): ScannedPdfUpload { if (pdfUri.scheme != ContentResolver.SCHEME_FILE) { return External(pdfUri, remoteFolderPath) diff --git a/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt b/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt index f971012b5d76..f23424fd3fdd 100644 --- a/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt +++ b/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt @@ -38,14 +38,20 @@ class ScannedPdfUploadTest { fun contentSchemeUriIsTreatedAsExternalScan() { val uri = uri("content", "/scan.pdf") - assertEquals(ScannedPdfUpload.External(uri, "/Documents/"), ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf")) + assertEquals( + ScannedPdfUpload.External(uri, "/Documents/"), + ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf") + ) } @Test fun uriWithoutSchemeIsTreatedAsExternalScan() { val uri = uri(null, null) - assertEquals(ScannedPdfUpload.External(uri, "/Documents/"), ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf")) + assertEquals( + ScannedPdfUpload.External(uri, "/Documents/"), + ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf") + ) } @Test