From 8e9b0d59904d99cf3f4cded0941fcdb5d417f30f Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:44:56 +0000 Subject: [PATCH 01/13] 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 | 123 ------------------ .../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, 300 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 f400bd5392b5..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt +++ /dev/null @@ -1,123 +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 - -@Suppress("Detekt.LongParameterList") // constructed only from factory method and tests -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 1fe963ad7c34..d0a8bf6ddd50 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 @@ -69,7 +67,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 @@ -106,7 +103,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) @@ -292,17 +288,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 da7d997f592c..3ba5d2631429 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -163,8 +163,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 86b49c3b47fb..c9e40fb89425 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 @@ -98,7 +97,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" @@ -827,24 +825,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 d6aa0898face..ee332a5a7b37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1455,9 +1455,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 c4f349637ac0ebc5d8d56ea0d326d85216244cca Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:48:01 +0000 Subject: [PATCH 02/13] 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 | 207 ------------------ .../documentscan/DocumentScanViewModel.kt | 189 ---------------- .../fragment/OCFileListBottomSheetActions.kt | 1 - .../fragment/OCFileListBottomSheetDialog.kt | 11 +- .../ui/fragment/OCFileListFragment.java | 23 +- .../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(+), 612 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 70e39466ee99..8767648d704f 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 @@ -433,7 +431,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 @@ -503,11 +500,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) @@ -523,8 +515,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 f20c2687a55f..4ecdcd11bd8f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -666,10 +666,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 7424948cae90..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt +++ /dev/null @@ -1,207 +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.utils.theme.ViewThemeUtils -import javax.inject.Inject - -class DocumentScanActivity : - ToolbarActivity(), - Injectable { - - @Inject - lateinit var vmFactory: ViewModelFactory - - @Inject - lateinit var logger: Logger - - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - - @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) - - setupToolbar() - 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 72655d3142d1..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt +++ /dev/null @@ -1,189 +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 - -@Suppress("Detekt.LongParameterList") // satisfied by DI -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 9466a9fd71a2..d2637f5a6cea 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 @@ -59,8 +58,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 { @@ -397,12 +395,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.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index fb7c402ac904..f77104a6b8ba 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -44,8 +44,6 @@ 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.editimage.EditImageActivity; import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.network.ClientFactory; @@ -201,7 +199,6 @@ public class OCFileListFragment extends ExtendedListFragment implements @Inject EditorUtils editorUtils; @Inject ShortcutUtil shortcutUtil; @Inject SyncedFolderProvider syncedFolderProvider; - @Inject AppScanOptionalFeature appScanOptionalFeature; @Inject ThumbnailGenerator thumbnailGenerator; @Inject AvatarGenerator avatarGenerator; @@ -566,8 +563,7 @@ public void registerFabListener() { currentDir, themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature); + editorUtils); dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); dialog.getBehavior().setSkipCollapsed(true); @@ -638,23 +634,6 @@ private void showDirectCameraUploadAlertDialog(FileDisplayActivity fileDisplayAc builder.show(); } - @Override - public void scanDocUpload() { - FileDisplayActivity fileDisplayActivity = (FileDisplayActivity) getActivity(); - - final OCFile currentFile = getCurrentFile(); - if (fileDisplayActivity != null && currentFile != null && currentFile.isFolder()) { - - Intent intent = new Intent(requireContext(), DocumentScanActivity.class); - intent.putExtra(DocumentScanActivity.EXTRA_FOLDER, currentFile.getRemotePath()); - startActivity(intent); - } else { - Log.w(TAG, "scanDocUpload: Failed to start doc scanning, fileDisplayActivity=" + fileDisplayActivity + - ", currentFile=" + currentFile); - SnackbarUtil.show(this, R.string.error_starting_doc_scan); - } - } - @Override public void 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 ee332a5a7b37..7a85ad23eee8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1453,12 +1453,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 0d583f5d7646fcbef8d091f4a2138c10241a657a Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:49:12 +0000 Subject: [PATCH 03/13] 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 | 20 ------ .../com/nextcloud/appscan/AppScanActivity.kt | 67 ------------------- .../com/nextcloud/appscan/ScanPageContract.kt | 27 -------- gradle/libs.versions.toml | 2 - settings.gradle.kts | 2 +- 12 files changed, 1 insertion(+), 351 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 d35263c357b1..30f3632e9bc8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -342,13 +342,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 @@ -443,13 +436,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 5be0703c66b4..000000000000 --- a/appscan/src/main/AndroidManifest.xml +++ /dev/null @@ -1,20 +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 4e0f1f417538..000000000000 --- a/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt +++ /dev/null @@ -1,67 +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.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Bundle -import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding -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(savedInstanceState) - DocumentScanner.init(this) - addFragmentContentLayout() - } - - override fun onError(error: DocumentScannerErrorModel) { - // TODO pass this from app somehow? - println(error) - } - - override fun onSuccess(scannerResults: ScannerResults) { - val intent = Intent() - - intent.putExtra( - EXTRA_FILE, - scannerResults.transformedImageFile?.absolutePath ?: scannerResults.croppedImageFile?.absolutePath - ) - - setResult(Activity.RESULT_OK, intent) - finish() - } - - override fun onClose() { - setResult(Activity.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" - } -} 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 aa5379befbcd..6998dcb3ec34 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 89f0af446dfc71608520f285a48511f84cb40458 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Thu, 24 Sep 2026 11:31:32 +0200 Subject: [PATCH 04/13] WIP feat(scan): Include FairScan via mavenLocal 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, but nothing actually ensures that the build machine actually has FairScan build locally, via ./gradlew publishToMavenLocal Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Philipp Hasper --- app/build.gradle.kts | 4 ++++ app/src/main/AndroidManifest.xml | 10 +++++++++- gradle.properties | 6 ++++++ gradle/libs.versions.toml | 3 +++ settings.gradle.kts | 11 +++++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 30f3632e9bc8..0002056be081 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -436,6 +436,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 4ecdcd11bd8f..321d2c2b6850 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 23:54:22 +0200 Subject: [PATCH 05/13] WIP feat(scan): CI builds Fairscan to local maven This is WIP, because in the final integration, FairScan should be available on a remote package registry and doesn't need to be build locally. 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 + 8 files changed, 56 insertions(+) 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 5406dd1f93ec..26338183cb7a 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 d1366f66d511..d2fc3617438a 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -40,6 +40,7 @@ jobs: - uses: gradle/actions/wrapper-validation@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - name: Setup Gradle uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + - 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 dc3171de27fd..4c2e9d6cf41b 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@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + - 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 5c8ee3aa7871..d74a66f70dfd 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 8d738f4a4850..aaeb9c5d1edb 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: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 42c2cb09a3e5..9e61e4b0df1b 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 b68fb42b9ff2..9d2b7c614b47 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@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + - uses: ./.github/actions/publish-fairscan-to-maven-local + - name: Run unit tests with coverage run: ./gradlew jacocoTestGplayDebugUnitTest From 3303a7f35dd28eb82ac52e42fe1c16f6bf077d60 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Thu, 24 Sep 2026 22:45:58 +0200 Subject: [PATCH 06/13] 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 +++------- .../ui/fragment/OCFileListFragment.java | 21 +++++++++---------- 4 files changed, 15 insertions(+), 27 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 8767648d704f..7dc03e9ba20b 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 @@ -432,8 +432,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 d2637f5a6cea..d8bc42b7590b 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 @@ -395,14 +395,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.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index f77104a6b8ba..6cfd54c6c870 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -119,6 +119,8 @@ import com.owncloud.android.utils.WebViewUtil; import com.owncloud.android.utils.theme.ThemeUtils; +import org.fairscan.app.FairScan; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -636,18 +638,15 @@ private void showDirectCameraUploadAlertDialog(FileDisplayActivity fileDisplayAc @Override public void scanDocUploadFromApp() { - requireActivity().startActivityForResult( - scanIntentExternalApp, - FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME); - } + final Activity activity = requireActivity(); - @Override - public boolean isScanDocUploadFromAppAvailable() { - var context = getActivity(); - if (context == null) { - return false; - } - return scanIntentExternalApp.resolveActivity(context.getPackageManager()) != null; + // 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. + final Intent intent = scanIntentExternalApp.resolveActivity(activity.getPackageManager()) != null + ? scanIntentExternalApp + : FairScan.scanToPdfIntent(activity); + + activity.startActivityForResult(intent, FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME); } @Override From 993a6ae4f16cd1c24e3bddb52e0622251c83da70 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 30 Sep 2026 07:08:54 +0000 Subject: [PATCH 07/13] 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 | 28 +------ .../ui/fragment/OCFileListFragment.java | 84 +++++++++++++++++-- 2 files changed, 83 insertions(+), 29 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 307f5ccc4e12..5d5fd4bc7c17 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 @@ -46,7 +46,6 @@ import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.SearchView -import androidx.core.util.Function import androidx.core.view.MenuItemCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible @@ -170,12 +169,10 @@ import com.owncloud.android.utils.PermissionUtil.requestNotificationPermission import com.owncloud.android.utils.PermissionUtil.requestStoragePermissionIfNeeded import com.owncloud.android.utils.PushUtils import com.owncloud.android.utils.StringUtils -import com.owncloud.android.utils.UriUtils import com.owncloud.android.utils.theme.CapabilityUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.apache.commons.io.FilenameUtils import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -1050,13 +1047,10 @@ class FileDisplayActivity : */ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (data != null && - ( - requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS || - requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME - ) && + requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) ) { - requestUploadOfContentFromApps(requestCode, resultCode, data) + requestUploadOfContentFromApps(resultCode, data) } else if (data != null && requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && ( @@ -1202,7 +1196,7 @@ class FileDisplayActivity : } } - private fun requestUploadOfContentFromApps(requestCode: Int, resultCode: Int, contentIntent: Intent) { + private fun requestUploadOfContentFromApps(resultCode: Int, contentIntent: Intent) { val streamsToUpload = ArrayList() if (contentIntent.clipData != null && (contentIntent.clipData?.itemCount ?: 0) > 0) { @@ -1224,17 +1218,6 @@ class FileDisplayActivity : val currentDir = getCurrentDir() val remotePath = if (currentDir != null) currentDir.remotePath else OCFile.ROOT_PATH - var fileDisplayNameTransformer: Function? = null - if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME) { - fileDisplayNameTransformer = { uri: Uri -> - val displayName = UriUtils.getDisplayNameForUri(uri, applicationContext) - if (displayName != null && displayName.isNotEmpty()) { - FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) - } else { - null - } - } - } val uploader = UriUploader( this, @@ -1246,7 +1229,7 @@ class FileDisplayActivity : behaviour, false, // Not show waiting dialog while file is being copied from private storage null, // Not needed copy temp task listener - fileDisplayNameTransformer + null ) uploader.uploadUris() @@ -3394,9 +3377,6 @@ class FileDisplayActivity : @JvmField val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA: Int = REQUEST_CODE__LAST_SHARED + 6 - @JvmField - val REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME: Int = REQUEST_CODE__LAST_SHARED + 7 - private val TAG: String = FileDisplayActivity::class.java.getSimpleName() const val TAG_LIST_OF_FILES: String = "LIST_OF_FILES" diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 6cfd54c6c870..b5b9b0a71dd5 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -19,13 +19,15 @@ 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; +import android.os.Parcelable; import android.text.TextUtils; -import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -46,10 +48,13 @@ import com.nextcloud.client.di.Injectable; import com.nextcloud.client.editimage.EditImageActivity; import com.nextcloud.client.jobs.BackgroundJobManager; +import com.nextcloud.client.jobs.upload.FileUploadHelper; +import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks; import com.nextcloud.client.utils.Throttler; import com.nextcloud.common.NextcloudClient; +import com.nextcloud.model.OCUploadLocalPathData; import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.ui.sort.SortOrderUi; @@ -112,6 +117,7 @@ import com.owncloud.android.ui.events.SearchEvent; 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.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; @@ -135,6 +141,8 @@ import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -232,6 +240,11 @@ public class OCFileListFragment extends ExtendedListFragment implements private static final Intent scanIntentExternalApp = new Intent("org.fairscan.app.action.SCAN_TO_PDF"); + private final ActivityResultLauncher scanLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> handleScanResult(result.getResultCode(), result.getData()) + ); + @Inject DeviceInfo deviceInfo; protected enum MenuItemAddRemove { @@ -642,11 +655,72 @@ public void scanDocUploadFromApp() { // 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. - final Intent intent = scanIntentExternalApp.resolveActivity(activity.getPackageManager()) != null - ? scanIntentExternalApp - : FairScan.scanToPdfIntent(activity); + boolean separateApp = scanIntentExternalApp.resolveActivity(activity.getPackageManager()) != null; + Intent scanIntent = separateApp ? scanIntentExternalApp : FairScan.scanToPdfIntent(activity, true); + + scanLauncher.launch(scanIntent); + } + + private void handleScanResult(int resultCode, @Nullable Intent data) { + Uri pdfUri = FairScan.pdfUriFromResult(resultCode, data); + if (pdfUri == null) { + return; + } + + uploadScannedPdf(pdfUri); + } + + private void uploadScannedPdf(@NonNull Uri pdfUri) { + if (!(getActivity() instanceof FileActivity fileActivity)) { + Log_OC.e(TAG, "Activity is null, cant upload scanned document"); + return; + } + + final var user = fileActivity.getUser(); + if (user.isEmpty()) { + Log_OC.e(TAG, "User not exist, cant upload scanned document"); + return; + } + + final OCFile currentDir = getCurrentFile(); + final String remotePath = currentDir != null ? currentDir.getRemotePath() : ROOT_PATH; + + if (ContentResolver.SCHEME_FILE.equals(pdfUri.getScheme())) { + uploadEmbeddedScannedPdf(user.get(), pdfUri, remotePath); + } else { + uploadExternalScannedPdf(fileActivity, user.get(), pdfUri, remotePath); + } + } - activity.startActivityForResult(intent, FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME); + private void uploadEmbeddedScannedPdf(@NonNull User user, @NonNull Uri pdfUri, @NonNull String remotePath) { + final String localPath = pdfUri.getPath(); + if (localPath == null) { + Log_OC.e(TAG, "Scanned document has no local path, cant upload: " + pdfUri); + return; + } + + final String remoteFilePath = remotePath + FileOperationsHelper.getTimestampedFileName(".pdf"); + OCUploadLocalPathData data = OCUploadLocalPathData.Companion.forFile( + user, + new String[] { localPath }, + new String[] { remoteFilePath }, + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + ); + FileUploadHelper.Companion.instance().uploadNewFiles(data); + } + + private void uploadExternalScannedPdf(@NonNull FileActivity fileActivity, @NonNull User user, @NonNull Uri pdfUri, + @NonNull String remotePath) { + new UriUploader( + fileActivity, + new ArrayList(List.of(pdfUri)), + remotePath, + user, + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + false, + null, + null + ).uploadUris(); } @Override From 63873dda9b6f72d4ad33072985958e5fd7b9203a Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 14 Oct 2026 19:12:41 +0200 Subject: [PATCH 08/13] 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 aa089d3fdafb..81ff68bb624b 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -9376,3 +9376,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 9b740a28ae0c10af8ba41e2ce8e524f6d4811a08 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 14 Oct 2026 19:58:48 +0200 Subject: [PATCH 09/13] 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 81ff68bb624b..af64a8e82b28 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 C3BAB45F4AF71FAB uid Yang Song @@ -2942,6 +2987,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) @@ -3312,6 +3401,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----- @@ -3580,6 +3702,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) @@ -9376,18 +9513,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 ec5d08302e60..e17ef3bc5721 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21,6 +21,8 @@ + + @@ -54,12 +56,14 @@ + + @@ -84,6 +88,7 @@ + @@ -91,6 +96,7 @@ + @@ -106,6 +112,7 @@ + @@ -373,6 +380,7 @@ + @@ -447,6 +455,7 @@ + @@ -540,6 +549,7 @@ + @@ -552,6 +562,7 @@ + @@ -1645,6 +1656,11 @@ + + + + + @@ -7243,6 +7259,11 @@ + + + + + @@ -25255,6 +25276,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25604,6 +25665,14 @@ + + + + + + + + @@ -29900,6 +29969,14 @@ + + + + + + + + @@ -30425,6 +30502,14 @@ + + + + + + + + @@ -33517,6 +33602,19 @@ + + + + + + + + + + + + + From e027342c30e0f3323ebac982361a3fe85bb85700 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 18 Oct 2026 15:42:54 +0200 Subject: [PATCH 10/13] 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 26338183cb7a..5406dd1f93ec 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 d2fc3617438a..d1366f66d511 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -40,7 +40,6 @@ jobs: - uses: gradle/actions/wrapper-validation@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - name: Setup Gradle uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - - 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 4c2e9d6cf41b..dc3171de27fd 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@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - - 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 d74a66f70dfd..5c8ee3aa7871 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 aaeb9c5d1edb..8d738f4a4850 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: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 9e61e4b0df1b..42c2cb09a3e5 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 9d2b7c614b47..b68fb42b9ff2 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@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - - 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 89af825f642e..91e1b09dbad5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,8 +8,6 @@ androidImageCropperVersion = "4.7.0" androidLibraryVersion ="dedf04f10c5e4f400baf9760993b5a2c7561ad58" androidOpensslVersion = "3.5.6.1" androidPluginVersion = "9.4.1" -# 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.0" @@ -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 f1dec694695bc5e47377cdbe3d37ed7895d5a059 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Tue, 20 Oct 2026 09:14:35 +0200 Subject: [PATCH 11/13] 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. --- gradle/verification-metadata.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e17ef3bc5721..7b7d2ae5b8cd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -37,6 +37,7 @@ + @@ -24865,6 +24866,22 @@ + + + + + + + + + + + + + + + + From e6f6d2c3f66c804667223c03ff73cef7259e8bec Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 21 Oct 2026 08:42:05 +0200 Subject: [PATCH 12/13] 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 ++++++++++ .../ui/fragment/OCFileListFragment.java | 34 ++++++------ .../client/scan/ScannedPdfUploadTest.kt | 55 +++++++++++++++++++ 3 files changed, 101 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.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index b5b9b0a71dd5..8c330e974e44 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -19,7 +19,6 @@ 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; @@ -51,6 +50,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.network.ClientFactory; +import com.nextcloud.client.scan.ScannedPdfUpload; import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks; import com.nextcloud.client.utils.Throttler; import com.nextcloud.common.NextcloudClient; @@ -685,36 +685,34 @@ private void uploadScannedPdf(@NonNull Uri pdfUri) { final OCFile currentDir = getCurrentFile(); final String remotePath = currentDir != null ? currentDir.getRemotePath() : ROOT_PATH; - if (ContentResolver.SCHEME_FILE.equals(pdfUri.getScheme())) { - uploadEmbeddedScannedPdf(user.get(), pdfUri, remotePath); - } else { - uploadExternalScannedPdf(fileActivity, user.get(), pdfUri, remotePath); - } - } + final String fileName = FileOperationsHelper.getTimestampedFileName(".pdf"); + final ScannedPdfUpload upload = ScannedPdfUpload.Companion.from(pdfUri, remotePath, fileName); - private void uploadEmbeddedScannedPdf(@NonNull User user, @NonNull Uri pdfUri, @NonNull String remotePath) { - final String localPath = pdfUri.getPath(); - if (localPath == null) { + if (upload instanceof ScannedPdfUpload.Embedded embedded) { + uploadEmbeddedScannedPdf(user.get(), embedded); + } else if (upload instanceof ScannedPdfUpload.External external) { + uploadExternalScannedPdf(fileActivity, user.get(), external); + } else { Log_OC.e(TAG, "Scanned document has no local path, cant upload: " + pdfUri); - return; } + } - final String remoteFilePath = remotePath + FileOperationsHelper.getTimestampedFileName(".pdf"); + private void uploadEmbeddedScannedPdf(@NonNull User user, @NonNull ScannedPdfUpload.Embedded upload) { OCUploadLocalPathData data = OCUploadLocalPathData.Companion.forFile( user, - new String[] { localPath }, - new String[] { remoteFilePath }, + new String[] { upload.getLocalPath() }, + new String[] { upload.getRemoteFilePath() }, FileUploadWorker.LOCAL_BEHAVIOUR_MOVE ); FileUploadHelper.Companion.instance().uploadNewFiles(data); } - private void uploadExternalScannedPdf(@NonNull FileActivity fileActivity, @NonNull User user, @NonNull Uri pdfUri, - @NonNull String remotePath) { + private void uploadExternalScannedPdf(@NonNull FileActivity fileActivity, @NonNull User user, + @NonNull ScannedPdfUpload.External external) { new UriUploader( fileActivity, - new ArrayList(List.of(pdfUri)), - remotePath, + new ArrayList(List.of(external.getUri())), + external.getRemoteFolderPath(), user, FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, 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 1ec6dbba763fc11501d1002673db8f88e4efe19f Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 21 Oct 2026 22:46:05 +0200 Subject: [PATCH 13/13] 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