From 035716e05da6a2690c7bc33137b2a653e71dc7ce 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 | 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 573811352c5d..cee5678bc68e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -27,7 +27,6 @@ import androidx.work.workDataOf import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker @@ -97,7 +96,6 @@ internal class BackgroundJobManagerImpl( const val JOB_FILES_UPLOAD = "files_upload" const val JOB_FOLDER_DOWNLOAD = "folder_download" const val JOB_FILES_DOWNLOAD = "files_download" - const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_OFFLINE_OPERATIONS = "offline_operations" @@ -826,24 +824,6 @@ internal class BackgroundJobManagerImpl( workManager.cancelAllWorkByTag(formatFileDownloadTag(accountName, fileId)) } - override fun startPdfGenerateAndUploadWork( - user: User, - uploadFolder: String, - imagePaths: List, - pdfPath: String - ) { - val data = workDataOf( - GeneratePdfFromImagesWork.INPUT_IMAGE_FILE_PATHS to imagePaths.toTypedArray(), - GeneratePdfFromImagesWork.INPUT_OUTPUT_FILE_PATH to pdfPath, - GeneratePdfFromImagesWork.INPUT_UPLOAD_ACCOUNT to user.accountName, - GeneratePdfFromImagesWork.INPUT_UPLOAD_FOLDER to uploadFolder - ) - val request = oneTimeRequestBuilder(GeneratePdfFromImagesWork::class, JOB_PDF_GENERATION) - .setInputData(data) - .build() - workManager.enqueue(request) - } - override fun scheduleTestJob() { val request = periodicRequestBuilder(TestJob::class, JOB_TEST) .setInitialDelay(DEFAULT_IMMEDIATE_JOB_DELAY_SEC, TimeUnit.SECONDS) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3320a7bb1929..ad359ff23be1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1454,9 +1454,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 ae426931c2cab279c574c5e9a80defcd57324173 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.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 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 22a2c841ab65..ef4cd5a74eb4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -669,10 +669,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 cb38a8f7d60d..659b853c193a 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; @@ -199,7 +197,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; @@ -564,8 +561,7 @@ public void registerFabListener() { currentDir, themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature); + editorUtils); dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); dialog.getBehavior().setSkipCollapsed(true); @@ -636,23 +632,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 ad359ff23be1..6c6a0869e7bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1452,12 +1452,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 5b4cf00d0ab73d1cce9dd4a1f67550f07100b9c2 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 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 5549ae0df79e..f30fe2406c13 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 5b28a1216ed803e65ff0f6176292a96c9c296d73 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 | 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 ef4cd5a74eb4..7c22a93cbd5f 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 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 1d3a6d8d89c2..328278559f23 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 508208b279d9..9d66699439fd 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 e53563189333..8a27668bcd7e 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 0b9b1ffc8b96..8188487bf805 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 e9e5baa0fbb10f56341bde0d1d5d5e5c3da0d3bb Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Thu, 24 Sep 2026 22:45:58 +0200 Subject: [PATCH 6/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 659b853c193a..d5ff947f69a2 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 @@ -118,6 +118,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; @@ -634,18 +636,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 8d600bbbfeec75935820a4c70fece8a8dca83f7a Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 30 Sep 2026 07:08:54 +0000 Subject: [PATCH 7/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 | 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 593c9c60e9cd..74892d22fa1f 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 @@ -1051,13 +1048,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 && ( @@ -1203,7 +1197,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) { @@ -1225,17 +1219,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, @@ -1247,7 +1230,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() @@ -3395,9 +3378,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 d5ff947f69a2..0d837546434a 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; @@ -111,6 +116,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; @@ -133,6 +139,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; @@ -230,6 +238,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 { @@ -640,11 +653,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 a4fc46e734e160f296527f0456075f4aeb994704 Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 14 Oct 2026 19:12:41 +0200 Subject: [PATCH 8/9] 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 7320e71d53c1421a5a20a69dc4021d161ecf514e Mon Sep 17 00:00:00 2001 From: Philipp Hasper Date: Wed, 14 Oct 2026 19:58:48 +0200 Subject: [PATCH 9/9] 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----- + +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 +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----- + +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 +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 dbf78ed17227..5ea88a7d9e9d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21,6 +21,8 @@ + + @@ -53,12 +55,14 @@ + + @@ -81,6 +85,7 @@ + @@ -88,6 +93,7 @@ + @@ -103,6 +109,7 @@ + @@ -370,6 +377,7 @@ + @@ -444,6 +452,7 @@ + @@ -537,6 +546,7 @@ + @@ -549,6 +559,7 @@ + @@ -1641,6 +1652,11 @@ + + + + + @@ -7239,6 +7255,11 @@ + + + + + @@ -25206,6 +25227,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25555,6 +25616,14 @@ + + + + + + + + @@ -29851,6 +29920,14 @@ + + + + + + + + @@ -30376,6 +30453,14 @@ + + + + + + + + @@ -33468,6 +33553,19 @@ + + + + + + + + + + + + +