From 04ef8eb1660fb0cd75bf308bb6e8b8506265674e Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:44:56 +0000 Subject: [PATCH 1/9] 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 | 61 --------- .../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, 299 deletions(-) delete mode 100644 app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt delete mode 100644 app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt diff --git a/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt b/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt deleted file mode 100644 index 36402fb52d5f..000000000000 --- a/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.graphics.pdf.PdfRenderer -import android.os.ParcelFileDescriptor -import com.nextcloud.client.logger.Logger -import com.owncloud.android.AbstractIT -import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import java.io.File - -internal class GeneratePDFUseCaseTest : AbstractIT() { - - @MockK - private lateinit var logger: Logger - - private lateinit var sut: GeneratePDFUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - sut = GeneratePDFUseCase(logger) - } - - @Test - fun invalidArguments_shouldReturnFalse() { - var result = sut.execute(emptyList(), "/test/foo.pdf") - assertFalse("Usecase does not indicate failure with invalid arguments", result) - result = sut.execute(listOf("/test.jpg"), "") - assertFalse("Usecase does not indicate failure with invalid arguments", result) - } - - @Test - fun generatePdf_checkPages() { - // can't think of how to test the _content_ of the pages - val images = listOf( - getFile("image.jpg"), - getFile("christine.jpg") - ).map { it.path } - - val output = "/sdcard/test.pdf" - - val result = sut.execute(images, output) - - assertTrue("Usecase does not indicate success", result) - - val outputFile = File(output) - - assertTrue("Output file does not exist", outputFile.exists()) - - ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { - PdfRenderer(it).use { renderer -> - val pageCount = renderer.pageCount - assertTrue("Page count is not correct", pageCount == 2) - } - } - - // clean up - outputFile.delete() - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt deleted file mode 100644 index 0f6cfa220e22..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt +++ /dev/null @@ -1,61 +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) - } - } - - 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 00cded05f561..a411e5889416 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 122b5a8ddd59..c37d5b80d0c2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -27,7 +27,6 @@ import androidx.work.workDataOf import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker @@ -97,7 +96,6 @@ internal class BackgroundJobManagerImpl( const val JOB_FILES_UPLOAD = "files_upload" const val JOB_FOLDER_DOWNLOAD = "folder_download" const val JOB_FILES_DOWNLOAD = "files_download" - const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_OFFLINE_OPERATIONS = "offline_operations" @@ -827,24 +825,6 @@ internal class BackgroundJobManagerImpl( workManager.cancelAllWorkByTag(startFileDownloadJobTag(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 c18bcf8427e0..e7f7aca89307 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 38adf117c8f72429f638fdf96ed1e06782644f10 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:48:01 +0000 Subject: [PATCH 2/9] 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.java | 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 9112027bde61..e8ce0a8842ad 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.ChooseAccountDialogFragment import com.nextcloud.ui.ChooseAccountDialogFragment.Companion.newInstance import com.nextcloud.ui.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 9020a664b936..ec633dc35f6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -672,10 +672,6 @@ android:name=".ui.preview.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 e6c773841bce..761177a393c4 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; @@ -198,7 +196,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; @@ -563,8 +560,7 @@ public void registerFabListener() { currentDir, themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature); + editorUtils); dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); dialog.getBehavior().setSkipCollapsed(true); @@ -635,23 +631,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 e7f7aca89307..dbcbb6a41a21 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 a458c1f119011e450db0a1c279869440f43a506a Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:49:12 +0000 Subject: [PATCH 3/9] 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 9db2148e375c..48b13fc1d392 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -347,13 +347,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 @@ -451,13 +444,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 de3a4edd9688..9e20fd2f36ca 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppComponent.kt +++ b/app/src/main/java/com/nextcloud/client/di/AppComponent.kt @@ -46,7 +46,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 dcfb44c7df3f..5a0512c9578a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,6 @@ daggerVersion = "2.60.1" detektGradlePlugin = "1.23.8" dexopenerVersion = "2.0.5" disklrucacheVersion = "2.0.2" -documentScannerVersion = "1.3.0" emojiGoogleVersion = "0.24.1" espressoVersion = "3.7.0" eventbusVersion = "3.3.1" @@ -101,7 +100,6 @@ browser = { module = "androidx.browser:browser", version.ref = "browserVersion" appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appCompatVersion" } cardview = { module = "androidx.cardview:cardview", version.ref = "cardviewVersion" } 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 e1dc80482a7e4de7141e09edc79fac298ec13e0b Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Thu, 24 Sep 2026 11:31:32 +0200 Subject: [PATCH 4/9] 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 | 6 +++++- gradle.properties | 6 ++++++ gradle/libs.versions.toml | 3 +++ settings.gradle.kts | 11 +++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 48b13fc1d392..3ffb2ea86fd0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -444,6 +444,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 ec633dc35f6b..4a27f970e4cc 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,9 @@ android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_config" /> + + Date: Thu, 24 Sep 2026 23:54:22 +0200 Subject: [PATCH 5/9] 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 a88af53ea3bc..521f4b5bd553 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 a6ed7cc24dff..8b073afeebed 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 6c6f76e82bb2..7af07f0930be 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 43b9ef84a446..5ddc136db845 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 fcb60bc8b79b..b2b039c16f38 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 3b3f80cc0172..0de76480cce2 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -37,6 +37,10 @@ jobs: distribution: "temurin" java-version: 21 + - 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 11f934a1da3b..48c216afccf5 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 9f280aa0bf984d59e68be67648d3d2254569e24b Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Sun, 20 Sep 2026 09:50:50 +0000 Subject: [PATCH 6/9] WIP - updated verification-metadata, part 1 Ran `./gradlew --write-verification-metadata pgp,sha256 --export-keys help` as per https://developer.android.com/build/dependency-verification#bootstrap_the_checksum_and_signature_data --- gradle/verification-keyring.keys | 48 +++++++++++ gradle/verification-metadata.xml | 140 ++++++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index aa089d3fdafb..b4452b075188 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -3312,6 +3312,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 +3613,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) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 228a08fcfcbe..8c465b2262b7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21,6 +21,9 @@ + + + @@ -53,12 +56,14 @@ + + @@ -81,6 +86,7 @@ + @@ -88,6 +94,7 @@ + @@ -103,6 +110,7 @@ + @@ -451,6 +459,7 @@ + @@ -556,6 +565,7 @@ + @@ -1648,6 +1658,11 @@ + + + + + @@ -7246,6 +7261,11 @@ + + + + + @@ -22360,10 +22380,10 @@ - + - + @@ -25223,6 +25243,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25572,6 +25632,14 @@ + + + + + + + + @@ -29166,6 +29234,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -29868,6 +29975,14 @@ + + + + + + + + @@ -30393,6 +30508,14 @@ + + + + + + + + @@ -33485,6 +33608,19 @@ + + + + + + + + + + + + + From 6e4da94b6397c30991613a4da29d4b75bb6d20b6 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Fri, 25 Sep 2026 10:34:18 +0200 Subject: [PATCH 7/9] WIP - updated verification-metadata, part 2 Ran `./gradlew --write-verification-metadata sha256 -Dorg.gradle.dependency.verification=lenient help && ./gradlew --export-keys` --- gradle/verification-metadata.xml | 872 +++++++++++++++++++++++++++++++ 1 file changed, 872 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8c465b2262b7..c9cf3ffdc451 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -635,6 +635,14 @@ + + + + + + + + @@ -740,6 +748,14 @@ + + + + + + + + @@ -803,6 +819,14 @@ + + + + + + + + @@ -1290,6 +1314,22 @@ + + + + + + + + + + + + + + + + @@ -1306,6 +1346,14 @@ + + + + + + + + @@ -1322,6 +1370,14 @@ + + + + + + + + @@ -1330,6 +1386,14 @@ + + + + + + + + @@ -1346,6 +1410,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -2723,6 +2811,14 @@ + + + + + + + + @@ -5820,6 +5916,14 @@ + + + + + + + + @@ -6131,6 +6235,14 @@ + + + + + + + + @@ -6283,6 +6395,14 @@ + + + + + + + + @@ -6435,6 +6555,14 @@ + + + + + + + + @@ -6587,6 +6715,14 @@ + + + + + + + + @@ -6739,6 +6875,14 @@ + + + + + + + + @@ -6891,6 +7035,14 @@ + + + + + + + + @@ -7059,6 +7211,14 @@ + + + + + + + + @@ -7069,6 +7229,11 @@ + + + + + @@ -7085,6 +7250,14 @@ + + + + + + + + @@ -7095,6 +7268,11 @@ + + + + + @@ -7111,6 +7289,14 @@ + + + + + + + + @@ -7129,6 +7315,11 @@ + + + + + @@ -7145,6 +7336,14 @@ + + + + + + + + @@ -7163,6 +7362,11 @@ + + + + + @@ -7179,6 +7383,14 @@ + + + + + + + + @@ -7189,6 +7401,11 @@ + + + + + @@ -7197,6 +7414,14 @@ + + + + + + + + @@ -7229,6 +7454,14 @@ + + + + + + + + @@ -7253,6 +7486,14 @@ + + + + + + + + @@ -7265,6 +7506,9 @@ + + + @@ -9191,6 +9435,19 @@ + + + + + + + + + + + + + @@ -10330,6 +10587,14 @@ + + + + + + + + @@ -10514,6 +10779,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -12889,6 +13180,14 @@ + + + + + + + + @@ -13041,6 +13340,14 @@ + + + + + + + + @@ -13071,6 +13378,11 @@ + + + + + @@ -13223,6 +13535,14 @@ + + + + + + + + @@ -13253,6 +13573,11 @@ + + + + + @@ -13405,6 +13730,14 @@ + + + + + + + + @@ -13557,6 +13890,14 @@ + + + + + + + + @@ -13709,6 +14050,14 @@ + + + + + + + + @@ -13781,6 +14130,14 @@ + + + + + + + + @@ -13933,6 +14290,14 @@ + + + + + + + + @@ -14085,6 +14450,14 @@ + + + + + + + + @@ -14237,6 +14610,14 @@ + + + + + + + + @@ -14389,6 +14770,14 @@ + + + + + + + + @@ -14541,6 +14930,14 @@ + + + + + + + + @@ -14693,6 +15090,14 @@ + + + + + + + + @@ -14845,6 +15250,14 @@ + + + + + + + + @@ -15144,6 +15557,14 @@ + + + + + + + + @@ -15296,6 +15717,14 @@ + + + + + + + + @@ -15448,6 +15877,14 @@ + + + + + + + + @@ -15600,6 +16037,14 @@ + + + + + + + + @@ -15752,6 +16197,14 @@ + + + + + + + + @@ -15904,6 +16357,14 @@ + + + + + + + + @@ -16056,6 +16517,14 @@ + + + + + + + + @@ -16264,6 +16733,14 @@ + + + + + + + + @@ -16416,6 +16893,14 @@ + + + + + + + + @@ -16456,6 +16941,14 @@ + + + + + + + + @@ -16608,6 +17101,14 @@ + + + + + + + + @@ -16760,6 +17261,14 @@ + + + + + + + + @@ -16940,6 +17449,14 @@ + + + + + + + + @@ -17156,6 +17673,14 @@ + + + + + + + + @@ -17228,6 +17753,14 @@ + + + + + + + + @@ -17300,6 +17833,14 @@ + + + + + + + + @@ -17452,6 +17993,14 @@ + + + + + + + + @@ -17524,6 +18073,14 @@ + + + + + + + + @@ -17596,6 +18153,14 @@ + + + + + + + + @@ -17668,6 +18233,14 @@ + + + + + + + + @@ -17740,6 +18313,14 @@ + + + + + + + + @@ -17892,6 +18473,14 @@ + + + + + + + + @@ -18044,6 +18633,14 @@ + + + + + + + + @@ -20956,6 +21553,14 @@ + + + + + + + + @@ -20972,6 +21577,14 @@ + + + + + + + + @@ -25387,6 +26000,14 @@ + + + + + + + + @@ -35779,21 +36400,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -35804,11 +36450,26 @@ + + + + + + + + + + + + + + + @@ -35817,11 +36478,21 @@ + + + + + + + + + + @@ -35830,6 +36501,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -35843,11 +36539,21 @@ + + + + + + + + + + @@ -35856,6 +36562,21 @@ + + + + + + + + + + + + + + + @@ -35869,6 +36590,11 @@ + + + + + @@ -35882,6 +36608,11 @@ + + + + + @@ -35895,11 +36626,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -35908,11 +36677,21 @@ + + + + + + + + + + @@ -35921,6 +36700,11 @@ + + + + + @@ -35934,11 +36718,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -35947,6 +36759,11 @@ + + + + + @@ -41155,6 +41972,11 @@ + + + + + @@ -41163,6 +41985,19 @@ + + + + + + + + + + + + + @@ -41171,6 +42006,14 @@ + + + + + + + + @@ -42511,6 +43354,14 @@ + + + + + + + + @@ -43415,6 +44266,27 @@ + + + + + + + + + + + + + + + + + + + + + From 17a0e652b4eb39bbfd62c71ff78325c379928144 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Thu, 24 Sep 2026 22:45:58 +0200 Subject: [PATCH 8/9] 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 e8ce0a8842ad..3454087cc0b5 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 761177a393c4..ac841f0854a4 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 @@ -117,6 +117,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; @@ -633,18 +635,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 9e710b08e3a4b44cc023bf2398db020214877a22 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 30 Sep 2026 07:08:54 +0000 Subject: [PATCH 9/9] 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 | 59 +++++++++++++++++-- 2 files changed, 58 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 27d5ebeea7b7..17fb053f1b6f 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 @@ -1035,13 +1032,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 && ( @@ -1187,7 +1181,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) { @@ -1209,17 +1203,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, @@ -1231,7 +1214,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() @@ -3368,9 +3351,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 ac841f0854a4..ba6f4fd7a5ba 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 @@ -21,11 +21,11 @@ import android.app.Activity; 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.text.TextUtils; -import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -46,9 +46,12 @@ 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.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; @@ -132,6 +135,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; @@ -229,6 +234,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 { @@ -639,11 +649,50 @@ 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(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; + } - activity.startActivityForResult(intent, FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME); + final String localPath = pdfUri.getPath(); + if (localPath == null) { + Log_OC.e(TAG, "Scanned document has no local path, cant upload: " + pdfUri); + return; + } + + final OCFile currentDir = getCurrentFile(); + final String remotePath = currentDir != null ? currentDir.getRemotePath() : ROOT_PATH; + final String remoteFilePath = remotePath + FileOperationsHelper.getTimestampedFileName(".pdf"); + + OCUploadLocalPathData data = OCUploadLocalPathData.Companion.forFile( + user.get(), + new String[] { localPath }, + new String[] { remoteFilePath }, + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + ); + FileUploadHelper.Companion.instance().uploadNewFiles(data); } @Override