diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c822052f1d13..163c6e2f29cd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -337,13 +337,6 @@ tasks.named("check").configure { val kspConfiguration = "ksp" val kspAndroidTestConfiguration = "kspAndroidTest" val gplayImplementationConfiguration = "gplayImplementation" -val huaweiImplementationConfiguration = "huaweiImplementation" -val qaImplementationConfiguration = "qaImplementation" -val appScanConfigurations = listOf( - gplayImplementationConfiguration, - huaweiImplementationConfiguration, - qaImplementationConfiguration -) dependencies { // region Nextcloud library @@ -439,11 +432,8 @@ 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")) - } + // region FairScan + implementation(libs.fairscan) // endregion // region SpotBugs 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/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt index dd06bdc081f9..c7c88229ad46 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt @@ -19,7 +19,6 @@ import android.os.Looper import android.view.ViewGroup import android.webkit.SslErrorHandler import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContract import androidx.fragment.app.DialogFragment import androidx.test.core.app.ActivityScenario import androidx.test.core.app.launchActivity @@ -37,7 +36,6 @@ import com.nextcloud.android.lib.resources.profile.HoverCard import com.nextcloud.client.account.RegisteredUser import com.nextcloud.client.account.Server import com.nextcloud.client.device.DeviceInfo -import com.nextcloud.client.documentscan.AppScanOptionalFeature import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment.Companion.newInstance import com.nextcloud.ui.dialog.account.onlineStatus.SetOnlineStatusBottomSheet @@ -432,10 +430,7 @@ 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 override fun showTemplate(creator: Creator?, headline: String?) = Unit override fun createRichWorkspace() = Unit } @@ -502,11 +497,6 @@ class DialogFragmentIT : AbstractIT() { } CapabilityUtils.updateCapability(capability) - val appScanOptionalFeature: AppScanOptionalFeature = object : AppScanOptionalFeature() { - override fun getScanContract(): ActivityResultContract = - throw UnsupportedOperationException("Document scan is not available") - } - val viewThemeUtils = ViewThemeUtils( materialSchemesForCurrentUser, ColorUtil(targetContext) @@ -522,8 +512,7 @@ class DialogFragmentIT : AbstractIT() { ocFile, fda.themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature + editorUtils ) sut.show() diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt new file mode 100644 index 000000000000..db8fdc830b1b --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentScanIT.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.os.SystemClock +import androidx.test.core.app.launchActivity +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.test.TestActivity +import com.owncloud.android.AbstractIT +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.nextcloud.client.jobs.upload.FileUploadWorker +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test + +class OCFileListFragmentScanIT : AbstractIT() { + + private lateinit var uploadsStorageManager: UploadsStorageManager + + @Before + fun setUp() { + Intents.init() + uploadsStorageManager = UploadsStorageManager( + UserAccountManagerImpl.fromContext(targetContext), + targetContext.contentResolver + ) + uploadsStorageManager.removeUploads(account.name) + } + + @After + fun tearDown() { + Intents.release() + uploadsStorageManager.removeUploads(account.name) + } + + @Test + fun scanStartsEmbeddedFairScanWithAutoDeletionDisabled() { + scan(Activity.RESULT_CANCELED, null) + + val scanIntent = Intents.getIntents().single { it.action == FAIRSCAN_ACTION } + assertEquals(FAIRSCAN_ACTIVITY, scanIntent.component?.className) + assertEquals(FAIRSCAN_ACTION, scanIntent.action) + assertTrue(scanIntent.getBooleanExtra(FAIRSCAN_EXTRA_DISABLE_AUTO_DELETION, false)) + } + + @Test + fun cancelledScanDoesNotUpload() { + scan(Activity.RESULT_CANCELED, null) + SystemClock.sleep(NO_UPLOAD_GRACE_MILLIS) + + assertEquals(0, uploadsStorageManager.getAllStoredUploads().size) + } + + @Test + fun embeddedScanResultIsQueuedForUploadWithMoveBehaviour() { + val pdf = createFile("scan.pdf", 1) + scan(Activity.RESULT_OK, Intent().putExtra(FAIRSCAN_EXTRA_PDF_PATH, pdf.absolutePath)) + + val upload = waitForUpload() + assertEquals(pdf.absolutePath, upload.localPath) + assertTrue(upload.remotePath.startsWith("/")) + assertTrue(upload.remotePath.endsWith(".pdf")) + assertEquals(FileUploadWorker.LOCAL_BEHAVIOUR_MOVE, upload.localAction) + assertEquals(account.name, upload.accountName) + } + + private fun scan(resultCode: Int, data: Intent?) { + launchActivity().use { scenario -> + intending(hasAction(FAIRSCAN_ACTION)).respondWith(Instrumentation.ActivityResult(resultCode, data)) + scenario.onActivity { activity -> + val fragment = OCFileListFragment() + activity.addFragment(fragment) + activity.supportFragmentManager.executePendingTransactions() + fragment.scanDocUploadFromApp() + } + SystemClock.sleep(RESULT_DELIVERY_MILLIS) + } + } + + private fun waitForUpload(): OCUpload { + val deadline = SystemClock.elapsedRealtime() + WAIT_TIMEOUT_MILLIS + while (SystemClock.elapsedRealtime() < deadline) { + uploadsStorageManager.getAllStoredUploads().firstOrNull()?.let { return it } + SystemClock.sleep(WAIT_POLL_INTERVAL_MILLIS) + } + fail("No upload was queued for the scanned PDF") + error("unreachable") + } + + private companion object { + const val FAIRSCAN_ACTIVITY = "org.fairscan.app.MainActivity" + const val FAIRSCAN_ACTION = "org.fairscan.app.action.SCAN_TO_PDF" + const val FAIRSCAN_EXTRA_DISABLE_AUTO_DELETION = "org.fairscan.app.extra.DISABLE_AUTO_DELETION" + const val FAIRSCAN_EXTRA_PDF_PATH = "org.fairscan.app.extra.PDF_PATH" + const val RESULT_DELIVERY_MILLIS = 1_000L + const val NO_UPLOAD_GRACE_MILLIS = 1_000L + const val WAIT_TIMEOUT_MILLIS = 10_000L + const val WAIT_POLL_INTERVAL_MILLIS = 200L + } +} diff --git a/app/src/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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8014d33ba07c..84c1e4a9fbd1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,8 @@ + tools:ignore="ScopedStorage" + tools:replace="android:maxSdkVersion" /> @@ -131,6 +132,13 @@ android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_config" /> + + + + - - * 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/di/ViewModelModule.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt index 0dc9da9cb15f..f08bcddb6a07 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt @@ -10,7 +10,6 @@ package com.nextcloud.client.di import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.nextcloud.client.documentscan.DocumentScanViewModel import com.nextcloud.client.etm.EtmViewModel import com.nextcloud.client.logger.ui.LogsViewModel import com.nextcloud.client.player.ui.audio.AudioPlayerViewModel @@ -52,11 +51,6 @@ abstract class ViewModelModule { @ViewModelKey(FileActionsViewModel::class) abstract fun fileActionsViewModel(vm: FileActionsViewModel): ViewModel - @Binds - @IntoMap - @ViewModelKey(DocumentScanViewModel::class) - abstract fun documentScanViewModel(vm: DocumentScanViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(TrashbinFileActionsViewModel::class) 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/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt deleted file mode 100644 index 17ff411712db..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt +++ /dev/null @@ -1,44 +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.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import coil.load -import com.owncloud.android.databinding.DocumentPageItemBinding - -class DocumentPageListAdapter : - ListAdapter(DiffItemCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocumentPageViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = DocumentPageItemBinding.inflate(inflater, parent, false) - return DocumentPageViewHolder(binding) - } - - override fun onBindViewHolder(holder: DocumentPageViewHolder, position: Int) { - holder.bind(currentList[position]) - } - - override fun getItemCount(): Int = currentList.size - - class DocumentPageViewHolder(val binding: DocumentPageItemBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(imagePath: String) { - binding.root.load(imagePath) - } - } - - private class DiffItemCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem - - override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt deleted file mode 100644 index a545cee8acc4..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.os.Bundle -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AlertDialog -import androidx.core.view.MenuProvider -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.nextcloud.client.di.Injectable -import com.nextcloud.client.di.ViewModelFactory -import com.nextcloud.client.logger.Logger -import com.owncloud.android.R -import com.owncloud.android.databinding.ActivityDocumentScanBinding -import com.owncloud.android.databinding.DialogScanExportTypeBinding -import com.owncloud.android.ui.activity.ToolbarActivity -import com.owncloud.android.ui.navigation.model.ActionBarStyle -import javax.inject.Inject - -class DocumentScanActivity : - ToolbarActivity(), - Injectable { - - @Inject - lateinit var vmFactory: ViewModelFactory - - @Inject - lateinit var logger: Logger - - @Inject - lateinit var appScanOptionalFeature: AppScanOptionalFeature - - lateinit var binding: ActivityDocumentScanBinding - - lateinit var viewModel: DocumentScanViewModel - - private var scanPage: ActivityResultLauncher? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - scanPage = registerForActivityResult(appScanOptionalFeature.getScanContract()) { result -> - viewModel.onScanPageResult(result) - } - - val folder = intent.extras?.getString(EXTRA_FOLDER) - require(folder != null) { "Folder must be provided for upload" } - - viewModel = ViewModelProvider(this, vmFactory)[DocumentScanViewModel::class.java] - viewModel.setUploadFolder(folder) - - setupViews() - - observeState() - } - - private fun setupViews() { - binding = ActivityDocumentScanBinding.inflate(layoutInflater) - setContentView(binding.root) - - showActionBar(ActionBarStyle.Plain) - supportActionBar?.let { - it.setDisplayHomeAsUpEnabled(true) - it.setDisplayShowHomeEnabled(true) - viewThemeUtils.files.themeActionBar(this, it) - } - - viewThemeUtils.material.themeFAB(binding.fab) - binding.fab.setOnClickListener { - viewModel.onAddPageClicked() - } - - binding.pagesRecycler.layoutManager = GridLayoutManager(this, PAGE_COLUMNS) - - setupMenu() - } - - private fun setupMenu() { - addMenuProvider( - object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.activity_document_scan, menu) - menu.findItem(R.id.action_save)?.let { - viewThemeUtils.platform.colorToolbarMenuIcon(this@DocumentScanActivity, it) - } - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.action_save -> { - viewModel.onClickDone() - true - } - - android.R.id.home -> { - onBackPressedDispatcher.onBackPressed() - true - } - - else -> false - } - } - ) - } - - private fun observeState() { - viewModel.uiState.observe(this, ::handleState) - } - - private fun handleState(state: DocumentScanViewModel.UIState) { - logger.d(TAG, "handleState: called with $state") - when (state) { - is DocumentScanViewModel.UIState.BaseState -> when (state) { - is DocumentScanViewModel.UIState.NormalState -> { - updateButtonsEnabled(true) - val pageList = state.pageList - updateRecycler(pageList) - if (state.shouldRequestScan) { - startPageScan() - } - } - - is DocumentScanViewModel.UIState.RequestExportState -> { - updateButtonsEnabled(false) - if (state.shouldRequestExportType) { - showExportDialog() - viewModel.onRequestTypeHandled() - } - } - } - - DocumentScanViewModel.UIState.DoneState, DocumentScanViewModel.UIState.CanceledState -> { - finish() - } - } - } - - private fun showExportDialog() { - val dialogBinding = DialogScanExportTypeBinding.inflate(layoutInflater) - - val dialog = MaterialAlertDialogBuilder(this) - .setTitle(R.string.document_scan_export_dialog_title) - .setCancelable(true) - .setView(dialogBinding.root) - .setNegativeButton(R.string.common_cancel) { _, _ -> - viewModel.onExportCanceled() - } - .setOnCancelListener { viewModel.onExportCanceled() } - .also { - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this@DocumentScanActivity, it) - } - .create() - - viewThemeUtils.platform.colorTextButtons(dialogBinding.btnPdf, dialogBinding.btnImages) - - dialogBinding.btnPdf.setOnClickListener { - viewModel.onExportTypeSelected(DocumentScanViewModel.ExportType.PDF) - dialog.dismiss() - } - dialogBinding.btnImages.setOnClickListener { - viewModel.onExportTypeSelected(DocumentScanViewModel.ExportType.IMAGES) - dialog.dismiss() - } - - dialog.setOnShowListener { - val alertDialog = it as AlertDialog - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)) - } - - dialog.show() - } - - private fun updateRecycler(pageList: List) { - if (binding.pagesRecycler.adapter == null) { - binding.pagesRecycler.adapter = DocumentPageListAdapter() - } - (binding.pagesRecycler.adapter as? DocumentPageListAdapter)?.submitList(pageList) - } - - private fun updateButtonsEnabled(enabled: Boolean) { - binding.fab.isEnabled = enabled - } - - private fun startPageScan() { - logger.d(TAG, "startPageScan() called") - viewModel.onScanRequestHandled() - scanPage!!.launch(Unit) - } - - companion object { - private const val TAG = "DocumentScanActivity" - private const val PAGE_COLUMNS = 2 - const val EXTRA_FOLDER = "extra_folder" - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt deleted file mode 100644 index a6a5d5062f46..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.nextcloud.client.account.CurrentAccountProvider -import com.nextcloud.client.di.IoDispatcher -import com.nextcloud.client.jobs.BackgroundJobManager -import com.nextcloud.client.jobs.upload.FileUploadHelper -import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.logger.Logger -import com.nextcloud.model.OCUploadLocalPathData -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.files.services.NameCollisionPolicy -import com.owncloud.android.operations.UploadFileOperation -import com.owncloud.android.ui.helpers.FileOperationsHelper -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.io.File -import javax.inject.Inject - -class DocumentScanViewModel @Inject constructor( - @IoDispatcher private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - app: Application, - private val logger: Logger, - private val backgroundJobManager: BackgroundJobManager, - private val currentAccountProvider: CurrentAccountProvider -) : AndroidViewModel(app) { - init { - logger.d(TAG, "DocumentScanViewModel created") - } - - sealed interface UIState { - sealed class BaseState(val pageList: List) : UIState { - val isEmpty: Boolean - get() = pageList.isEmpty() - } - - class NormalState(pageList: List = emptyList(), val shouldRequestScan: Boolean = false) : - BaseState(pageList) - - class RequestExportState(pageList: List = emptyList(), val shouldRequestExportType: Boolean = true) : - BaseState(pageList) - - object DoneState : UIState - object CanceledState : UIState - } - - private var uploadFolder: String? = null - private val initialState = UIState.NormalState(shouldRequestScan = true) - private val _uiState = MutableLiveData(initialState) - val uiState: LiveData - get() = _uiState - - /** - * @param result should be the path to the scanned page on the disk - */ - fun onScanPageResult(result: String?) { - logger.d(TAG, "onScanPageResult() called with: result = $result") - - val state = _uiState.value - require(state is UIState.NormalState) - - viewModelScope.launch(ioDispatcher) { - if (result != null) { - val newPath = renameCapturedImage(result) - val pageList = state.pageList.toMutableList() - pageList.add(newPath) - _uiState.postValue(UIState.NormalState(pageList)) - } else { - // result == null means cancellation or error - if (state.isEmpty) { - // close only if no pages have been added yet - _uiState.postValue(UIState.CanceledState) - } - } - } - } - - // TODO extract to usecase - private fun renameCapturedImage(originalPath: String): String { - val file = File(originalPath) - val renamedFile = - File( - getApplication().cacheDir.path + - File.separator + FileOperationsHelper.getCapturedImageName() - ) - file.renameTo(renamedFile) - return renamedFile.absolutePath - } - - fun onScanRequestHandled() { - val state = uiState.value - require(state is UIState.NormalState) - - _uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = false)) - } - - fun onAddPageClicked() { - val state = uiState.value - require(state is UIState.NormalState) - if (!state.shouldRequestScan) { - _uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = true)) - } - } - - fun onClickDone() { - val state = _uiState.value - if (state is UIState.BaseState && !state.isEmpty) { - _uiState.postValue(UIState.RequestExportState(state.pageList)) - } - } - - fun setUploadFolder(folder: String) { - this.uploadFolder = folder - } - - fun onRequestTypeHandled() { - val state = _uiState.value - require(state is UIState.RequestExportState) - _uiState.postValue(UIState.RequestExportState(state.pageList, false)) - } - - fun onExportTypeSelected(exportType: ExportType) { - val state = _uiState.value - require(state is UIState.RequestExportState) - when (exportType) { - ExportType.PDF -> { - exportToPdf(state.pageList) - } - - ExportType.IMAGES -> { - exportToImages(state.pageList) - } - } - _uiState.postValue(UIState.DoneState) - } - - private fun exportToPdf(pageList: List) { - val genPath = - getApplication().cacheDir.path + File.separator + FileOperationsHelper.getTimestampedFileName( - ".pdf" - ) - backgroundJobManager.startPdfGenerateAndUploadWork( - currentAccountProvider.user, - uploadFolder!!, - pageList, - genPath - ) - // after job is started, finish the application. - _uiState.postValue(UIState.DoneState) - } - - private fun exportToImages(pageList: List) { - val uploadPaths = pageList.map { - uploadFolder + OCFile.PATH_SEPARATOR + File(it).name - }.toTypedArray() - - val data = OCUploadLocalPathData.forDocument(currentAccountProvider.user, pageList.toTypedArray(), uploadPaths) - FileUploadHelper.instance().uploadNewFiles(data) - } - - fun onExportCanceled() { - val state = _uiState.value - if (state is UIState.BaseState) { - _uiState.postValue(UIState.NormalState(state.pageList)) - } - } - - private companion object { - private const val TAG = "DocumentScanViewModel" - } - - enum class ExportType { - PDF, - IMAGES - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt deleted file mode 100644 index 87bd71b87e17..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.graphics.BitmapFactory -import android.graphics.pdf.PdfDocument -import com.nextcloud.client.logger.Logger -import java.io.FileOutputStream -import java.io.IOException -import javax.inject.Inject - -/** - * This class takes a list of images and generates a PDF file. - */ -class GeneratePDFUseCase @Inject constructor(private val logger: Logger) { - /** - * @param imagePaths list of image paths - * @return `true` if the PDF was generated successfully, `false` otherwise - */ - fun execute(imagePaths: List, filePath: String): Boolean = if (imagePaths.isEmpty() || filePath.isBlank()) { - logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath") - false - } else { - val document = PdfDocument() - fillDocumentPages(document, imagePaths) - writePdfToFile(filePath, document) - } - - /** - * @return `true` if the PDF was generated successfully, `false` otherwise - */ - private fun writePdfToFile(filePath: String, document: PdfDocument): Boolean = try { - val fileOutputStream = FileOutputStream(filePath) - document.writeTo(fileOutputStream) - fileOutputStream.close() - document.close() - true - } catch (ex: IOException) { - logger.e(TAG, "Error generating PDF", ex) - false - } - - private fun fillDocumentPages(document: PdfDocument, imagePaths: List) { - imagePaths.forEach { path -> - val bitmap = BitmapFactory.decodeFile(path) - val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, 1).create() - val page = document.startPage(pageInfo) - page.canvas.drawBitmap(bitmap, 0f, 0f, null) - document.finishPage(page) - bitmap.recycle() - } - } - - companion object { - private const val TAG = "GeneratePDFUseCase" - } -} diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt deleted file mode 100644 index 7fa343cc19b0..000000000000 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Tobias Kaminsky - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.documentscan - -import android.app.NotificationManager -import android.content.Context -import android.graphics.BitmapFactory -import androidx.annotation.StringRes -import androidx.core.app.NotificationCompat -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.nextcloud.client.account.AnonymousUser -import com.nextcloud.client.account.User -import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.jobs.upload.FileUploadHelper -import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.logger.Logger -import com.nextcloud.model.OCUploadLocalPathData -import com.owncloud.android.R -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.files.services.NameCollisionPolicy -import com.owncloud.android.operations.UploadFileOperation -import com.owncloud.android.ui.notifications.NotificationUtils -import com.owncloud.android.utils.theme.ViewThemeUtils -import java.io.File -import java.security.SecureRandom - -class GeneratePdfFromImagesWork( - private val appContext: Context, - private val generatePdfUseCase: GeneratePDFUseCase, - private val viewThemeUtils: ViewThemeUtils, - private val notificationManager: NotificationManager, - private val userAccountManager: UserAccountManager, - private val logger: Logger, - params: WorkerParameters -) : Worker(appContext, params) { - - override fun doWork(): Result { - val inputPaths = inputData.getStringArray(INPUT_IMAGE_FILE_PATHS)?.toList() - val outputFilePath = inputData.getString(INPUT_OUTPUT_FILE_PATH) - val uploadFolder = inputData.getString(INPUT_UPLOAD_FOLDER) - val accountName = inputData.getString(INPUT_UPLOAD_ACCOUNT) - - @Suppress("Detekt.ComplexCondition") // not that complex - require(!inputPaths.isNullOrEmpty() && outputFilePath != null && uploadFolder != null && accountName != null) { - "PDF generation work started with missing parameters:" + - " inputPaths: $inputPaths, outputFilePath: $outputFilePath," + - " uploadFolder: $uploadFolder, accountName: $accountName" - } - - val user = userAccountManager.getUser(accountName) - require(user.isPresent && user.get() !is AnonymousUser) { "Invalid or not found user" } - - logger.d( - TAG, - "PDF generation work started with parameters: inputPaths=$inputPaths," + - "outputFilePath=$outputFilePath, uploadFolder=$uploadFolder, accountName=$accountName" - ) - - val notificationId = showNotification(R.string.document_scan_pdf_generation_in_progress) - val result = generatePdfUseCase.execute(inputPaths, outputFilePath) - notificationManager.cancel(notificationId) - if (result) { - uploadFile(user.get(), uploadFolder, outputFilePath) - cleanupImages(inputPaths) - } else { - logger.w(TAG, "PDF generation failed") - showNotification(R.string.document_scan_pdf_generation_failed) - return Result.failure() - } - - logger.d(TAG, "PDF generation work finished") - return Result.success() - } - - private fun cleanupImages(inputPaths: List) { - inputPaths.forEach { - val deleted = File(it).delete() - logger.d(TAG, "Deleted $it: success = $deleted") - } - } - - private fun showNotification(@StringRes messageRes: Int): Int { - val notificationId = SecureRandom().nextInt() - val message = appContext.getString(messageRes) - - val notificationBuilder = NotificationCompat.Builder( - appContext, - NotificationUtils.NOTIFICATION_CHANNEL_GENERAL - ) - .setSmallIcon(R.drawable.notification_icon) - .setLargeIcon(BitmapFactory.decodeResource(appContext.resources, R.drawable.notification_icon)) - .setContentText(message) - .setAutoCancel(true) - - viewThemeUtils.androidx.themeNotificationCompatBuilder(appContext, notificationBuilder) - - notificationManager.notify(notificationId, notificationBuilder.build()) - - return notificationId - } - - private fun uploadFile(user: User, uploadFolder: String, pdfPath: String) { - val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name - - val data = OCUploadLocalPathData.forDocument(user, arrayOf(pdfPath), arrayOf(uploadPath)) - FileUploadHelper().uploadNewFiles(data) - } - - companion object { - const val INPUT_IMAGE_FILE_PATHS = "input_image_file_paths" - const val INPUT_OUTPUT_FILE_PATH = "input_output_file_path" - const val INPUT_UPLOAD_FOLDER = "input_upload_folder" - const val INPUT_UPLOAD_ACCOUNT = "input_upload_account" - private const val TAG = "GeneratePdfFromImagesWo" - } -} diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b1eb3fec92f1..8721a7b01efb 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -20,8 +20,6 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.client.device.PowerManagementService -import com.nextcloud.client.documentscan.GeneratePDFUseCase -import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker @@ -68,7 +66,6 @@ class BackgroundJobFactory @Inject constructor( private val deckApi: DeckApi, private val viewThemeUtils: Provider, private val localBroadcastManager: Provider, - private val generatePdfUseCase: GeneratePDFUseCase, private val syncedFolderProvider: SyncedFolderProvider, private val database: NextcloudDatabase, private val uploadFileOperationFactory: UploadFileOperationFactory @@ -105,7 +102,6 @@ class BackgroundJobFactory @Inject constructor( FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) AlbumFileUploadWorker::class -> createAlbumsFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) - GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters) @@ -291,17 +287,6 @@ class BackgroundJobFactory @Inject constructor( params ) - private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork = - GeneratePdfFromImagesWork( - appContext = context, - generatePdfUseCase = generatePdfUseCase, - viewThemeUtils = viewThemeUtils.get(), - notificationManager = notificationManager, - userAccountManager = accountManager, - logger = logger, - params = params - ) - private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork = HealthStatusWork( context, params, diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 7f4a0a3184cc..8bead0cec495 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -161,8 +161,6 @@ interface BackgroundJobManager { conflictUploadId: Long? ) - fun startPdfGenerateAndUploadWork(user: User, uploadFolder: String, imagePaths: List, pdfPath: String) - fun scheduleTestJob() fun startImmediateTestJob() fun cancelTestJob() diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 54f3852d8763..8e80712cdd0a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -27,7 +27,6 @@ import androidx.work.workDataOf import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker @@ -97,7 +96,6 @@ internal class BackgroundJobManagerImpl( const val JOB_FILES_UPLOAD = "files_upload" const val JOB_FOLDER_DOWNLOAD = "folder_download" const val JOB_FILES_DOWNLOAD = "files_download" - const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_OFFLINE_OPERATIONS = "offline_operations" @@ -826,24 +824,6 @@ internal class BackgroundJobManagerImpl( workManager.cancelAllWorkByTag(formatFileDownloadTag(accountName, fileId)) } - override fun startPdfGenerateAndUploadWork( - user: User, - uploadFolder: String, - imagePaths: List, - pdfPath: String - ) { - val data = workDataOf( - GeneratePdfFromImagesWork.INPUT_IMAGE_FILE_PATHS to imagePaths.toTypedArray(), - GeneratePdfFromImagesWork.INPUT_OUTPUT_FILE_PATH to pdfPath, - GeneratePdfFromImagesWork.INPUT_UPLOAD_ACCOUNT to user.accountName, - GeneratePdfFromImagesWork.INPUT_UPLOAD_FOLDER to uploadFolder - ) - val request = oneTimeRequestBuilder(GeneratePdfFromImagesWork::class, JOB_PDF_GENERATION) - .setInputData(data) - .build() - workManager.enqueue(request) - } - override fun scheduleTestJob() { val request = periodicRequestBuilder(TestJob::class, JOB_TEST) .setInitialDelay(DEFAULT_IMMEDIATE_JOB_DELAY_SEC, TimeUnit.SECONDS) diff --git a/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt b/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt new file mode 100644 index 000000000000..e8a9b7fdf6d9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/scan/ScannedPdfUpload.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.scan + +import android.content.ContentResolver +import android.net.Uri + +sealed interface ScannedPdfUpload { + data class Embedded(val localPath: String, val remoteFilePath: String) : ScannedPdfUpload + + data class External(val uri: Uri, val remoteFolderPath: String) : ScannedPdfUpload + + data object Invalid : ScannedPdfUpload + + companion object { + @Suppress("ReturnCount") + fun from(pdfUri: Uri, remoteFolderPath: String, remoteFileName: String): ScannedPdfUpload { + if (pdfUri.scheme != ContentResolver.SCHEME_FILE) { + return External(pdfUri, remoteFolderPath) + } + + val localPath = pdfUri.path ?: return Invalid + return Embedded(localPath, remoteFolderPath + remoteFileName) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index c1616b00515d..f6c2c60f6097 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -1413,13 +1413,9 @@ class FileDisplayActivity : // region Activity results override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when { - data != null && requestCode in selectContentFromAppsRequestCodes && + data != null && requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS && resultCode in selectContentFromAppsResultCodes -> - uploadHandler.uploadContentFromApps( - data, - resultCode, - autoRename = requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME - ) + uploadHandler.uploadContentFromApps(data, resultCode) data != null && requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && resultCode in selectFilesFromFileSystemResultCodes -> @@ -2635,12 +2631,6 @@ class FileDisplayActivity : val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA = REQUEST_CODE__LAST_SHARED + 6 - val REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME = REQUEST_CODE__LAST_SHARED + 7 - - private val selectContentFromAppsRequestCodes = setOf( - REQUEST_CODE__SELECT_CONTENT_FROM_APPS, - REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME - ) private val selectContentFromAppsResultCodes = setOf(RESULT_OK, UploadFilesActivity.RESULT_OK_AND_MOVE) private val selectFilesFromFileSystemResultCodes = setOf( RESULT_OK, diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt index 2eeff506e423..d82b7e36999a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt @@ -7,9 +7,7 @@ package com.owncloud.android.ui.activity.filedisplayactivity import android.content.Intent -import android.net.Uri import android.os.Parcelable -import androidx.core.util.Function import androidx.lifecycle.lifecycleScope import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -28,11 +26,9 @@ import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailable import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.ui.helpers.UriUploader import com.owncloud.android.utils.StringUtils -import com.owncloud.android.utils.UriUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.apache.commons.io.FilenameUtils import java.io.File class FileDisplayUploadHandler( @@ -42,7 +38,7 @@ class FileDisplayUploadHandler( ) { // region Content from other apps - fun uploadContentFromApps(contentIntent: Intent, resultCode: Int, autoRename: Boolean) { + fun uploadContentFromApps(contentIntent: Intent, resultCode: Int) { val user = activity.user.orElse(null) ?: return val clipData = contentIntent.clipData @@ -59,7 +55,6 @@ class FileDisplayUploadHandler( } val remotePath = activity.getCurrentDir()?.remotePath ?: OCFile.ROOT_PATH - val displayNameTransformer = if (autoRename) Function { timestampedFileName(it) } else null UriUploader( activity, @@ -68,17 +63,9 @@ class FileDisplayUploadHandler( user, behaviour, showWaitingDialog = false, - copyTmpTaskListener = null, - fileDisplayNameTransformer = displayNameTransformer + copyTmpTaskListener = null ).uploadUris() } - - private fun timestampedFileName(uri: Uri): String? { - val displayName = UriUtils.getDisplayNameForUri(uri, activity.applicationContext) - if (displayName.isNullOrEmpty()) return null - - return FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) - } // endregion // region Camera diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.kt index 5f8a7df59994..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 @@ -33,18 +33,13 @@ interface OCFileListBottomSheetActions { fun newPresentation() fun directCameraUpload() - fun scanDocUpload() /** - * 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 22d4f255a9fb..559fb2eafecc 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt @@ -28,7 +28,6 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.AppScanOptionalFeature import com.nextcloud.utils.BuildHelper.isFlavourGPlay import com.nextcloud.utils.EditorUtils import com.nextcloud.utils.extensions.isNetworkAndServerAvailableSuspended @@ -58,8 +57,7 @@ class OCFileListBottomSheetDialog( private val file: OCFile, private val themeUtils: ThemeUtils, private val viewThemeUtils: ViewThemeUtils, - private val editorUtils: EditorUtils, - private val appScanOptionalFeature: AppScanOptionalFeature + private val editorUtils: EditorUtils ) : BottomSheetDialog(fileActivity), Injectable { @@ -396,19 +394,9 @@ class OCFileListBottomSheetDialog( dismiss() } - if (appScanOptionalFeature.isAvailable) { - menuScanDocUpload.setOnClickListener { - actions.scanDocUpload() - dismiss() - } - } else if (actions.isScanDocUploadFromAppAvailable) { - menuScanDocUpload.setOnClickListener { - actions.scanDocUploadFromApp() - dismiss() - } - } else { - menuScanDocUpload.visibility = View.GONE - menuScanDocUploadDivider.visibility = View.GONE + menuScanDocUpload.setOnClickListener { + actions.scanDocUploadFromApp() + dismiss() } menuUploadFiles.setOnClickListener { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 5e80cd21e333..3413989d81ac 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -21,6 +21,7 @@ import android.annotation.SuppressLint 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 @@ -29,6 +30,7 @@ import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.IdRes import androidx.annotation.OptIn import androidx.annotation.StringRes @@ -43,9 +45,11 @@ import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation import com.nextcloud.client.account.User import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable -import com.nextcloud.client.documentscan.AppScanOptionalFeature -import com.nextcloud.client.documentscan.DocumentScanActivity +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.scan.ScannedPdfUpload import com.nextcloud.client.utils.Throttler +import com.nextcloud.model.OCUploadLocalPathData import com.nextcloud.repository.ClientRepository import com.nextcloud.ui.fileactions.FileAction import com.nextcloud.ui.fileactions.FileActionHandler @@ -108,6 +112,7 @@ import com.owncloud.android.ui.events.SearchEvent import com.owncloud.android.ui.fragment.FileFragment.ContainerActivity import com.owncloud.android.ui.fragment.helper.ParentFolderFinder import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.ui.helpers.UriUploader import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface import com.owncloud.android.ui.navigation.model.ActionBarStyle import com.owncloud.android.utils.FileSortOrder @@ -117,6 +122,7 @@ import com.owncloud.android.utils.theme.ThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.fairscan.app.FairScan import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -157,9 +163,6 @@ open class OCFileListFragment : @Inject lateinit var fileActionHandler: FileActionHandler - @Inject - lateinit var appScanOptionalFeature: AppScanOptionalFeature - @Inject lateinit var thumbnailGenerator: ThumbnailGenerator @@ -216,6 +219,9 @@ open class OCFileListFragment : private var fabMain: FloatingActionButton? = null private val multiChoiceModeListener by lazy { OCFileListMultiChoiceModeListener(this) } private val mainHandler = Handler(Looper.getMainLooper()) + private val scanLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + handleScanResult(result.resultCode, result.data) + } private val activeActionMode: ActionMode? get() = multiChoiceModeListener.activeActionMode @@ -473,8 +479,7 @@ open class OCFileListFragment : currentDir, themeUtils, viewThemeUtils, - editorUtils, - appScanOptionalFeature + editorUtils ).run { behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.skipCollapsed = true @@ -574,37 +579,66 @@ open class OCFileListFragment : builder.show() } - override fun scanDocUpload() { - val fileDisplayActivity = activity as? FileDisplayActivity - val currentFile = currentFile - if (fileDisplayActivity == null || currentFile == null || !currentFile.isFolder) { - Log_OC.w( - TAG, - "Cannot start document scan: activity=${activity?.javaClass?.simpleName}, " + - "folder=${currentFile?.remotePath}, isFolder=${currentFile?.isFolder}" - ) - SnackbarUtil.show(this, R.string.error_starting_doc_scan) + override fun scanDocUploadFromApp() { + val activity = requireActivity() + + // A separately installed FairScan wins over the built-in scanner, so users can pick up + // a newer FairScan release before Nextcloud itself updates the bundled version. + val isSeparateAppInstalled = scanIntentExternalApp.resolveActivity(activity.packageManager) != null + val scanIntent = if (isSeparateAppInstalled) scanIntentExternalApp else FairScan.scanToPdfIntent(activity, true) + + scanLauncher.launch(scanIntent) + } + + private fun handleScanResult(resultCode: Int, data: Intent?) { + val pdfUri = FairScan.pdfUriFromResult(resultCode, data) ?: return + uploadScannedPdf(pdfUri) + } + + private fun uploadScannedPdf(pdfUri: Uri) { + val fileActivity = activity as? FileActivity + if (fileActivity == null) { + Log_OC.e(TAG, "Activity is null, cant upload scanned document") return } - val intent = Intent(requireContext(), DocumentScanActivity::class.java) - .putExtra(DocumentScanActivity.EXTRA_FOLDER, currentFile.remotePath) - startActivity(intent) + val user = fileActivity.user.getOrNull() + if (user == null) { + Log_OC.e(TAG, "User not exist, cant upload scanned document") + return + } + + val remotePath = currentFile?.remotePath ?: OCFile.ROOT_PATH + val fileName = FileOperationsHelper.getTimestampedFileName(PDF_EXTENSION) + + when (val upload = ScannedPdfUpload.from(pdfUri, remotePath, fileName)) { + is ScannedPdfUpload.Embedded -> uploadEmbeddedScannedPdf(user, upload) + is ScannedPdfUpload.External -> uploadExternalScannedPdf(fileActivity, user, upload) + ScannedPdfUpload.Invalid -> Log_OC.e(TAG, "Scanned document has no local path, cant upload: $pdfUri") + } } - @Suppress("DEPRECATION") - override fun scanDocUploadFromApp() { - requireActivity().startActivityForResult( - scanIntentExternalApp, - FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME + private fun uploadEmbeddedScannedPdf(user: User, upload: ScannedPdfUpload.Embedded) { + val data = OCUploadLocalPathData.forFile( + user, + arrayOf(upload.localPath), + arrayOf(upload.remoteFilePath), + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE ) + FileUploadHelper.instance().uploadNewFiles(data) } - override val isScanDocUploadFromAppAvailable: Boolean - get() { - val context = activity ?: return false - return scanIntentExternalApp.resolveActivity(context.packageManager) != null - } + private fun uploadExternalScannedPdf(fileActivity: FileActivity, user: User, external: ScannedPdfUpload.External) { + UriUploader( + fileActivity, + listOf(external.uri), + external.remoteFolderPath, + user, + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + showWaitingDialog = false, + copyTmpTaskListener = null + ).uploadUris() + } override fun uploadFiles() { val fileActivity = activity as? FileActivity @@ -1531,6 +1565,7 @@ open class OCFileListFragment : private const val ALL_MIME_TYPES = "*/*" private const val SCAN_TO_PDF_ACTION = "org.fairscan.app.action.SCAN_TO_PDF" + private const val PDF_EXTENSION = ".pdf" private const val ACTIVITY_TAB = 0 private const val SHARING_TAB = 1 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 f3d6f60bf3e1..5df5197b53b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1456,14 +1456,6 @@ Add end-to-end encryption to this client Add new secure file drop Not possible without internet connection - Scan page - Done - Generating PDF… - Error starting document scan - PDF generation failed - Choose export type - PDF file - Multiple images Cannot create local file Invalid filename for local file Team folders 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 diff --git a/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt b/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt new file mode 100644 index 000000000000..f23424fd3fdd --- /dev/null +++ b/app/src/test/java/com/nextcloud/client/scan/ScannedPdfUploadTest.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.scan + +import android.net.Uri +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +class ScannedPdfUploadTest { + + private fun uri(scheme: String?, path: String?): Uri = mockk { + every { this@mockk.scheme } returns scheme + every { this@mockk.path } returns path + } + + @Test + fun fileSchemeUriIsTreatedAsEmbeddedScan() { + val result = ScannedPdfUpload.from(uri("file", "/data/files/fairscan/scan.pdf"), "/Documents/", "scan.pdf") + + assertEquals(ScannedPdfUpload.Embedded("/data/files/fairscan/scan.pdf", "/Documents/scan.pdf"), result) + } + + @Test + fun embeddedScanInRootFolderUsesRootPath() { + val result = ScannedPdfUpload.from(uri("file", "/data/scan.pdf"), "/", "20260101_scan.pdf") + + assertEquals(ScannedPdfUpload.Embedded("/data/scan.pdf", "/20260101_scan.pdf"), result) + } + + @Test + fun contentSchemeUriIsTreatedAsExternalScan() { + val uri = uri("content", "/scan.pdf") + + assertEquals( + ScannedPdfUpload.External(uri, "/Documents/"), + ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf") + ) + } + + @Test + fun uriWithoutSchemeIsTreatedAsExternalScan() { + val uri = uri(null, null) + + assertEquals( + ScannedPdfUpload.External(uri, "/Documents/"), + ScannedPdfUpload.from(uri, "/Documents/", "scan.pdf") + ) + } + + @Test + fun fileSchemeUriWithoutPathIsInvalid() { + assertEquals(ScannedPdfUpload.Invalid, ScannedPdfUpload.from(uri("file", null), "/", "scan.pdf")) + } +} diff --git a/appscan/build.gradle.kts b/appscan/build.gradle.kts deleted file mode 100644 index 1ade447dfb0b..000000000000 --- a/appscan/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.android.library) -} - -android { - namespace = "com.nextcloud.appscan" - - defaultConfig { - minSdk = 28 - compileSdk = 37 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - - lint.targetSdk = 36 - testOptions.targetSdk = 36 -} - -kotlin.compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") -} - -dependencies { - implementation(libs.appcompat) - implementation(libs.document.scanning.android.sdk) { - exclude(group = "androidx.test", module = "monitor") - } - implementation(libs.ui) -} diff --git a/appscan/src/main/AndroidManifest.xml b/appscan/src/main/AndroidManifest.xml deleted file mode 100644 index df434e2fe8be..000000000000 --- a/appscan/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - diff --git a/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt b/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt deleted file mode 100644 index 58d3b4a99e50..000000000000 --- a/appscan/src/main/java/com/nextcloud/appscan/AppScanActivity.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-FileCopyrightText: 2023 Álvaro Brey - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.appscan - -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.PointF -import android.graphics.drawable.BitmapDrawable -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.core.view.children -import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.ScanActivity -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.ScannerResults -import com.zynksoftware.documentscanner.ui.DocumentScanner - -@Suppress("unused") -class AppScanActivity : ScanActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - applyEdgeToEdgeWithSystemBarPadding() - super.onCreate(null) - DocumentScanner.init(this) - addFragmentContentLayout() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - recreateCropFragmentView() - } - - private fun recreateCropFragmentView() { - val cropFragment = supportFragmentManager.findFragmentByTag(IMAGE_CROP_FRAGMENT_TAG) ?: return - if (!cropFragment.isVisible) { - return - } - - val cornerFractions = cropFragment.view?.let(::cropCornerFractions) - - supportFragmentManager.beginTransaction().detach(cropFragment).commitNow() - supportFragmentManager.beginTransaction().attach(cropFragment).commitNow() - - val cropView = cropFragment.view ?: return - if (cornerFractions == null) { - return - } - - cropView.post { restoreCropCorners(cropView, cornerFractions) } - } - - private fun cropCornerFractions(cropView: View): List? { - val preview = cropPreviewBitmap(cropView) ?: return null - return cropCornerHandles(cropView).map { PointF(it.x / preview.width, it.y / preview.height) } - } - - private fun restoreCropCorners(cropView: View, cornerFractions: List) { - val preview = cropPreviewBitmap(cropView) ?: return - cropCornerHandles(cropView).zip(cornerFractions) { handle, fraction -> - handle.x = fraction.x * preview.width - handle.y = fraction.y * preview.height - } - cropView.findViewById(R.id.polygonView).invalidate() - } - - private fun cropPreviewBitmap(cropView: View): Bitmap? = - (cropView.findViewById(R.id.imagePreview).drawable as? BitmapDrawable)?.bitmap - - private fun cropCornerHandles(cropView: View): List = - cropView.findViewById(R.id.polygonView).children.toList() - - override fun onError(error: DocumentScannerErrorModel) = Unit - - override fun onSuccess(scannerResults: ScannerResults) { - val intent = Intent() - - intent.putExtra( - EXTRA_FILE, - scannerResults.transformedImageFile?.absolutePath ?: scannerResults.croppedImageFile?.absolutePath - ) - - setResult(RESULT_OK, intent) - finish() - } - - override fun onClose() { - if (isChangingConfigurations) { - return - } - - setResult(RESULT_CANCELED) - finish() - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (permissions.contains(android.Manifest.permission.CAMERA) && - (grantResults.isEmpty() || grantResults[0] == PackageManager.PERMISSION_DENIED) - ) { - onClose() - } - } - - companion object { - @JvmStatic - val enabled: Boolean = true - - const val EXTRA_FILE = "file" - - private const val IMAGE_CROP_FRAGMENT_TAG = "ImageCropFragmentTag" - } -} diff --git a/appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt b/appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt deleted file mode 100644 index 2170bc0be9b6..000000000000 --- a/appscan/src/main/java/com/nextcloud/appscan/ScanPageContract.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.appscan // Note: if class package or name changes, you must adjust the app's VariantModule.kt - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContract - -@Suppress("unused") // Class is instantiated via reflection -class ScanPageContract : ActivityResultContract() { - override fun createIntent(context: Context, input: Unit): Intent { - return Intent(context, AppScanActivity::class.java) - } - - override fun parseResult(resultCode: Int, intent: Intent?): String? { - if (resultCode != Activity.RESULT_OK) { - return null - } - return intent?.getStringExtra(AppScanActivity.EXTRA_FILE) - } -} diff --git a/gradle.properties b/gradle.properties index 91f3dedc1c10..6540bbc337ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,12 @@ NC_TEST_SERVER_PASSWORD=test android.useAndroidX=true android.nonTransitiveRClass=true #android.debug.obsoleteApi=true +# com.google.ai.edge.litert:litert-support and :litert-support-api (pulled in by FairScan) both +# declare the manifest package org.tensorflow.lite.support, which AGP 9's strict per-library +# namespace-uniqueness check rejects. This is a confirmed, unfixed Google packaging bug with no +# ETA (google-ai-edge/LiteRT#6965) - excluding either artifact removes classes the other needs at +# runtime. Revisit once that's fixed upstream or litert-support ships a v2. +android.uniquePackageNames=false # JVM arguments to optimize heap usage, enable heap dump on out-of-memory errors, and set the file encoding diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6ce3a3298b6..7cc27a1fbfdc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,12 +26,12 @@ 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" exifinterfaceVersion = "1.4.2" ezVcardVersion = "0.12.2" +fairscanVersion = "v2.3.0-11-gc1517c5-lib-1" fbContribVersion = "7.7.4" findsecbugsPluginVersion = "1.14.0" firebaseMessagingVersion = "26.0.1" @@ -94,7 +94,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" } @@ -128,6 +127,7 @@ ext-strikethrough = { module = "io.noties.markwon:ext-strikethrough", version.re ext-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwonVersion" } ext-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref = "markwonVersion" } ez-vcard = { module = "com.googlecode.ez-vcard:ez-vcard", version.ref = "ezVcardVersion" } +fairscan = { module = "com.github.phillab.fairscan-library-fork:scan-ui", version.ref = "fairscanVersion" } findsecbugs-plugin = { module = "com.h3xstream.findsecbugs:findsecbugs-plugin", version.ref = "findsecbugsPluginVersion" } fb-contrib = { module = "com.mebigfatguy.fb-contrib:fb-contrib", version.ref = "fbContribVersion" } firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseMessagingVersion" } diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index aa7c1fa58232..641bda2615cb 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -2138,6 +2138,51 @@ u8veAZEXwuJGYHE= =gx4v -----END PGP PUBLIC KEY BLOCK----- +pub C0E2EB10E7B60FAD +uid Mike Penz (OpenSource library signing) + +sub 488E1BD9D30006C0 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +e31IKwLC/NOHo4/A +=SROI +-----END PGP PUBLIC KEY BLOCK----- + pub C2B98E64C61147AC sub 23B5E29FACD5C2B5 -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -2954,6 +2999,50 @@ tJscrv0/jtIwo5zy8SqldnhB6g== =OReJ -----END PGP PUBLIC KEY BLOCK----- +pub D7662013FA1A5596 +uid skydoves (Jaewoong Eum) + +sub CBC29FFF4999D93A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +MD9W3Ckbgcahr/9YO8OFLg== +=I2kh +-----END PGP PUBLIC KEY BLOCK----- + pub D7C92B70FA1C814D uid Matthew Sicker (Signing Key) @@ -3324,6 +3413,39 @@ u4awI7kDfdqz1V5s5prLGlkoPhACUxRZeznT3Js7NVE= =ZRYf -----END PGP PUBLIC KEY BLOCK----- +pub E0F9DC177239CD8A +sub 12F64A1C91895516 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBGWNTxIBDADmE/W3AVPhGWIHBGoXk4gwN0htHmclNqfoL5ghNS0GrMm6B99J +tRExRf45/cqo6pfWgZmPoxB/6BUBU96hb1pAdOPGjq4AyDgDwg9W/yvTPTRvnsds +SmIE35YlYN8LDXVgWAKEVOmNFOgFw+xDcDvKCUTrBq02cSfMMh8jfrbylCT2U0bD +hDqlOEJ0pTUmOsnNr0+6ds4FHCJneNR4jQKkI5g8qAALPKNRr66KTR5qckeL90xi +KMtbgzd4nBClfMfxrV7Ysw/xmriKqLK8q0A9A1xQwK5JYEIab6viVIvUD3IlWE0T +HLoPQQZGuYzICI4UHBQWN/aRA1HWruAL8ms4I+XS63r8LiDDyYgXlzDr0ZngB82S +RUpnJX9UEIZt6R0mURLJ8EoUfnP/wpb7rNeepuyvXDwQ9EaGXEUoI7ldJjWUeVpw ++zoPHFieXG2LjBzrhI5sIzd6FSt1UbvsAQnkuIZTGhDAlI4SkeyArFLgq1vsaRwv +bYoUpoyPyf2+158AEQEAAc7AzQRljU8SAQwAtmgIFYFPuTziWJvkrocnER2eSRun +Zp5DCxb+aGolVS521bebxcTXv+jcgpuzJGCXsU4yv8jQY2tgwXq+13nPw1GJdPn0 +TiqXNZRgY5/cwVVl7haawUXoXt8F/alH3ruyy4AUKyZIk1fO6Ald/LMEhDJKCr0z +s0YLqk8d1gG7y1Y9ORwwa1DIL+Y+NugmKOqzCLhOQ4ojvMHQ1xq/EgP91pgErznV +k65jx0+vKcrdkZiPQfZDIvDeXG/mu8UB5IzjUXbShG9T07VGmd87q9I8ZbGCwOR0 +GT3bs5sdX/IMUQs7HZUOERum5eDJ/e5bo9iWBY9ffGISKJQQ5jZsS1rwr2ioRjO3 +vfiWrT+0u12sxdGDxuaoWZkqmqcssmSCSiAKaWEtba6IZdm0V1ZuyUbODp9+8DqC +JsnMmMaGXC6GlkEdHW/0DkdqnRANRZ6UhU0q+pJExlc5rQichgpm0mk77HRsCDYN +Ga9ZUj/nJwJLotJFlRXVIIwljZ+wJfyWgRdvABEBAAHCwPwEGAEKACYWIQTtqFiD +iTwsoJgdWlTg+dwXcjnNigUCZY1PEgIbDAUJA8JnAAAKCRDg+dwXcjnNinhbC/9I +oYxAERxkBllxY5FaIWvVn35v3axQRCvqz8v1lgPL2LrIkokcmyZENbMoIaC25E8B +JKgQjJtLJKJzE5sOLQV/rYnHUSVwTn+mm8iyNu3VhRtfoh3uq4S+ZmFixs76xFla +em5tO9h/7UBH29OmDuYm3eQAZF/ASmXkvRuEOx3WtD6YFNUQ3V61t46/1PlIoZ9x +EA6WkrHL8z4+HEaL+ftCVGwL+F8q2Hs5UI9C/dnESYwenYOmMRssgSqp3+8T+00x +rWdGY/mNAUCTlSoAjsE5vFMz3dJZKMdpuuIrZMnTMh1Em0wa9+9yz8dzOicp02ld +M9fgmoXl4za2RZM6WtR74KYfroAu5YZm9sNkCPmT/fqJYotA2OJsq7e3x5PQKVP1 +BoYmenmPgRcrfZVJALQPKiunh9l5VYEq7DgTJ+520XG7brQKKxnp8wpIkXVcWSqz +ksd/LwATBPYf7rfKuxEWDfCy294FXhfUSAIpYlHvMSNvxqots45yDwNfSWpwJ/Y= +=nPBW +-----END PGP PUBLIC KEY BLOCK----- + pub E126833F9CF64915 sub C64121F5021754AB -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -3592,6 +3714,21 @@ kDPpOUpVEh9FB0MrCNxY+0pa =w5DF -----END PGP PUBLIC KEY BLOCK----- +pub E886D43F9B6F84F9 +uid Calvin Liang + +sub 70B51E4211514239 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEZVrPUxYJKwYBBAHaRw8BAQdAFtim0TLdBCDZca6SP9cArWjKwSo5PxZR1LcS +HzNgwZG0G0NhbHZpbiBMaWFuZyA8bWVAY2FsdmluLnNoPs44BGVaz1MSCisGAQQB +l1UBBQEBB0An+YxznGdlz0EGY6qQ8tyN4YOdHguNq5SaeqwIepeQcgMBCAfCfgQY +FgoAJhYhBKsH2OnyQ+P2+TO0+uiG1D+bb4T5BQJlWs9TAhsMBQkFo5qAAAoJEOiG +1D+bb4T55pUA/3GHc7qG/pHBXdDoPU+FWdHgP0+72TlApwuXUFIKKp8FAP4p3XaR +/uUcbNvsVCfpWVQEkVohY/V8EQ0RRvgRZgKHDA== +=vhh3 +-----END PGP PUBLIC KEY BLOCK----- + pub E93671C7272B7B3F uid Rolf Lear (JDOM) (Used to sign JDOM Packages) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8816c7adae60..f867034102e4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -22,6 +22,8 @@ + + @@ -36,6 +38,7 @@ + @@ -55,12 +58,14 @@ + + @@ -86,6 +91,7 @@ + @@ -93,6 +99,7 @@ + @@ -108,6 +115,7 @@ + @@ -384,6 +392,7 @@ + @@ -458,6 +467,7 @@ + @@ -551,6 +561,7 @@ + @@ -563,6 +574,7 @@ + @@ -1656,6 +1668,11 @@ + + + + + @@ -7259,6 +7276,11 @@ + + + + + @@ -24966,6 +24988,22 @@ + + + + + + + + + + + + + + + + @@ -25377,6 +25415,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25726,6 +25804,14 @@ + + + + + + + + @@ -30040,6 +30126,14 @@ + + + + + + + + @@ -30565,6 +30659,14 @@ + + + + + + + + @@ -33657,6 +33759,19 @@ + + + + + + + + + + + + + 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")