diff --git a/app/build.gradle b/app/build.gradle index a1ffcb87e1e6..59afb9ed9ec5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,6 +43,8 @@ apply plugin: 'pmd' apply from: "$rootProject.projectDir/jacoco.gradle" apply plugin: 'com.github.spotbugs' apply plugin: 'io.gitlab.arturbosch.detekt' +// apply scanbot SDK for NMC +apply from: "$rootProject.projectDir/nmc_scan-dependencies.gradle" // needed to make renovate run without shot, as shot requires Android SDK // https://github.com/pedrovgs/Shot/issues/300 @@ -199,6 +201,7 @@ android { resources { excludes += 'META-INF/LICENSE*' excludes += 'META-INF/versions/9/OSGI-INF/MANIFEST*' + excludes += 'META-INF/DEPENDENCIES' pickFirst 'MANIFEST.MF' // workaround for duplicated manifest on some dependencies } } @@ -318,10 +321,11 @@ dependencies { implementation 'com.vanniktech:emoji-google:0.21.0' implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + //NextCloud scan is not required in NMC // document scanner not available on FDroid (generic) due to OpenCV binaries - gplayImplementation project(':appscan') - huaweiImplementation project(':appscan') - qaImplementation project(':appscan') + /* gplayImplementation project(':appscan') + huaweiImplementation project(':appscan') + qaImplementation project(':appscan') */ spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.8' diff --git a/app/src/androidTest/java/com/nmc/android/scans/ScanActivityMultipleTest.kt b/app/src/androidTest/java/com/nmc/android/scans/ScanActivityMultipleTest.kt new file mode 100644 index 000000000000..e28c2ab5ddb7 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/scans/ScanActivityMultipleTest.kt @@ -0,0 +1,62 @@ +package com.nmc.android.scans + +import android.Manifest +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.rule.GrantPermissionRule +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import junit.framework.TestCase +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +/* + * Scan test to test the max number of possible scans till device throws exception or unexpected error occurs + */ +class ScanActivityMultipleTest : AbstractIT() { + @get:Rule + val activityRule = ActivityScenarioRule(ScanActivity::class.java) + + @get:Rule + val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA) + + private var docScanCount = 0 + + @Test + fun runAllScanTests() { + captureAndVerifyDocScan() + for (i in 0 until MAX_NUMBER_OF_SCAN) { + println("Scan no: $docScanCount") + verifyScanMoreDocument() + } + } + + private fun captureAndVerifyDocScan() { + Espresso.onView(ViewMatchers.withId(R.id.shutterButton)).perform(ViewActions.click()) + shortSleep() + shortSleep() + shortSleep() + shortSleep() + docScanCount++ + TestCase.assertEquals(docScanCount, ScanActivity.originalScannedImages.size) + } + + private fun verifyScanMoreDocument() { + Espresso.onView(ViewMatchers.withId(R.id.scanMoreButton)).perform(ViewActions.click()) + captureAndVerifyDocScan() + } + + companion object { + /** + * variable to define max number of scans to test + */ + private const val MAX_NUMBER_OF_SCAN = 40 + } +} diff --git a/app/src/androidTest/java/com/nmc/android/scans/ScanActivityTest.kt b/app/src/androidTest/java/com/nmc/android/scans/ScanActivityTest.kt new file mode 100644 index 000000000000..9fae7371ef3c --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/scans/ScanActivityTest.kt @@ -0,0 +1,246 @@ +package com.nmc.android.scans + +import android.Manifest +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.rule.GrantPermissionRule +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import junit.framework.TestCase +import org.hamcrest.core.IsNot +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +/* + *Scan test to test the full flow of document scan from Scanning to Save page. + */ +class ScanActivityTest : AbstractIT() { + @get:Rule + val activityRule = ActivityScenarioRule(ScanActivity::class.java) + + @get:Rule + val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA) + + private var docScanCount = 0 + + /* + * running all test in one test will create a flow from scanning to saving the scans + */ + @Test + fun runAllScanTests() { + verifyIfToolbarHidden() + verifyIfScanFragmentReplaced() + verifyToggleAutomatic() + verifyToggleFlash() + captureAndVerifyDocScan() + verifyScanMoreDocument() + verifyApplyFilter() + verifyRotateDocument() + verifyImageCrop() + verifyImageDeletion() + verifySaveScannedDocs() + verifyPasswordSwitch() + verifyPdfPasswordSwitchToggle() + } + + private fun verifyIfToolbarHidden() { + Espresso.onView(ViewMatchers.withId(R.id.toolbar)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isDisplayed()))) + } + + private fun verifyIfScanFragmentReplaced() { + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_automatic)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_flash)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_cancel)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.shutterButton)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + } + + private fun verifyToggleAutomatic() { + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_automatic)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_automatic)).check( + ViewAssertions.matches( + ViewMatchers.hasTextColor( + R.color.grey_60 + ) + ) + ) + + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_automatic)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_automatic)).check( + ViewAssertions.matches( + ViewMatchers.hasTextColor( + R.color.primary + ) + ) + ) + } + + private fun verifyToggleFlash() { + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_flash)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_flash)).check( + ViewAssertions.matches( + ViewMatchers.hasTextColor( + R.color.primary + ) + ) + ) + + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_flash)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_doc_btn_flash)).check( + ViewAssertions.matches( + ViewMatchers.hasTextColor( + R.color.grey_60 + ) + ) + ) + } + + private fun captureAndVerifyDocScan() { + Espresso.onView(ViewMatchers.withId(R.id.shutterButton)).perform(ViewActions.click()) + shortSleep() + shortSleep() + shortSleep() + docScanCount++ + TestCase.assertEquals(docScanCount, ScanActivity.originalScannedImages.size) + } + + private fun verifyScanMoreDocument() { + Espresso.onView(ViewMatchers.withId(R.id.scanMoreButton)).perform(ViewActions.click()) + captureAndVerifyDocScan() + } + + private fun verifyApplyFilter() { + Espresso.onView(ViewMatchers.withId(R.id.filterDocButton)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(R.string.edit_scan_filter_dialog_title)) + .inRoot(RootMatchers.isDialog()) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView(ViewMatchers.withText(R.string.edit_scan_filter_b_n_w)) + .inRoot(RootMatchers.isDialog()) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + .perform(ViewActions.click()) + + shortSleep() + shortSleep() + shortSleep() + } + + private fun verifyRotateDocument() { + Espresso.onView(ViewMatchers.withId(R.id.rotateDocButton)).perform(ViewActions.click()) + } + + private fun verifyImageCrop() { + Espresso.onView(ViewMatchers.withId(R.id.cropDocButton)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withId(R.id.crop_polygon_view)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.crop_btn_reset_borders)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView(ViewMatchers.withId(R.id.action_save)).perform(ViewActions.click()) + } + + private fun verifyImageDeletion() { + Espresso.onView(ViewMatchers.withId(R.id.deleteDocButton)).perform(ViewActions.click()) + docScanCount-- + TestCase.assertEquals(docScanCount, ScanActivity.originalScannedImages.size) + } + + private fun verifySaveScannedDocs() { + Espresso.onView(ViewMatchers.withId(R.id.action_save)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withId(R.id.scan_save_filename_input)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_location_input)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_nested_scroll_view)).perform(ViewActions.swipeUp()) + + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_pdf_checkbox)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_png_checkbox)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_jpg_checkbox)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_with_txt_recognition_pdf_checkbox)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_with_txt_recognition_txt_checkbox)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_pdf_checkbox)).check( + ViewAssertions.matches( + IsNot.not(ViewMatchers.isChecked()) + ) + ) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_png_checkbox)).check( + ViewAssertions.matches( + IsNot.not(ViewMatchers.isChecked()) + ) + ) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_jpg_checkbox)).check( + ViewAssertions.matches( + IsNot.not(ViewMatchers.isChecked()) + ) + ) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_with_txt_recognition_pdf_checkbox)) + .check(ViewAssertions.matches(ViewMatchers.isChecked())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_with_txt_recognition_txt_checkbox)).check( + ViewAssertions.matches( + IsNot.not(ViewMatchers.isChecked()) + ) + ) + + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(ViewMatchers.isEnabled())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isChecked()))) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_text_input)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isDisplayed()))) + + Espresso.onView(ViewMatchers.withId(R.id.save_scan_btn_cancel)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.save_scan_btn_save)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + } + + private fun verifyPasswordSwitch() { + Espresso.onView(ViewMatchers.withId(R.id.scan_save_with_txt_recognition_pdf_checkbox)) + .perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isEnabled()))) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isChecked()))) + + Espresso.onView(ViewMatchers.withId(R.id.scan_save_without_txt_recognition_pdf_checkbox)) + .perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(ViewMatchers.isEnabled())) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isChecked()))) + } + + private fun verifyPdfPasswordSwitchToggle() { + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_text_input)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_switch)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.scan_save_pdf_password_text_input)) + .check(ViewAssertions.matches(IsNot.not(ViewMatchers.isDisplayed()))) + } +} diff --git a/app/src/androidTest/java/com/nmc/android/scans/ScanbotIT.kt b/app/src/androidTest/java/com/nmc/android/scans/ScanbotIT.kt new file mode 100644 index 000000000000..90491590b9cb --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/scans/ScanbotIT.kt @@ -0,0 +1,93 @@ +package com.nmc.android.scans + +import android.content.Intent +import android.os.Looper +import androidx.activity.result.contract.ActivityResultContract +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.documentscan.AppScanOptionalFeature +import com.nextcloud.utils.EditorUtils +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions +import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +class ScanbotIT : AbstractIT() { + + @Mock + private lateinit var actions: OCFileListBottomSheetActions + + @get:Rule + var activityRule = IntentsTestRule(FileDisplayActivity::class.java, true, false) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun validateScanButton() { + //Looper to avoid android.util.AndroidRuntimeException: Animators may only be run on Looper threads + //during running test + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val intent = Intent(targetContext, FileDisplayActivity::class.java) + val fda = activityRule.launchActivity(intent) + val info = DeviceInfo() + val ocFile = OCFile("/test.md") + val appScanOptionalFeature: AppScanOptionalFeature = object : AppScanOptionalFeature() { + override fun getScanContract(): ActivityResultContract { + throw UnsupportedOperationException("Document scan is not available") + } + } + + val editorUtils = EditorUtils(ArbitraryDataProviderImpl(targetContext)) + val sut = OCFileListBottomSheetDialog( + fda, + actions, + info, + user, + ocFile, + fda.themeUtils, + activityRule.activity.viewThemeUtils, + editorUtils, + appScanOptionalFeature + ) + + fda.runOnUiThread { sut.show() } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + shortSleep() + + sut.behavior.state = BottomSheetBehavior.STATE_EXPANDED + + //validate nmc scan button visibility & clickable + onView(withId(R.id.menu_scan_document)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.menu_scan_document)).check(matches(isClickable())) + + //validate nc scan button hidden + onView(withId(R.id.menu_scan_doc_upload)).check(matches(not(isDisplayed()))) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + shortSleep() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java index eb14c5a7d3c9..02481dafe3ab 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java @@ -388,6 +388,11 @@ public void showTemplate(Creator creator, String headline) { public void createRichWorkspace() { } + + @Override + public void scanDocument() { + + } }; DeviceInfo info = new DeviceInfo(); diff --git a/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt b/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt index b909b6285c16..09baf76e982b 100644 --- a/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt +++ b/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt @@ -7,7 +7,6 @@ */ package com.nextcloud.client.di -import com.nextcloud.appscan.ScanPageContract import com.nextcloud.client.documentscan.AppScanOptionalFeature import dagger.Module import dagger.Provides @@ -18,8 +17,6 @@ internal class VariantModule { @Provides @Reusable fun scanOptionalFeature(): AppScanOptionalFeature { - return object : AppScanOptionalFeature() { - override fun getScanContract() = ScanPageContract() - } + return AppScanOptionalFeature.Stub } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d3bfe327d1e..881358891a44 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -107,6 +107,7 @@ + + + + diff --git a/app/src/main/assets/ocr_blobs/deu.traineddata b/app/src/main/assets/ocr_blobs/deu.traineddata new file mode 100644 index 000000000000..97ed7b2b60f2 Binary files /dev/null and b/app/src/main/assets/ocr_blobs/deu.traineddata differ diff --git a/app/src/main/assets/ocr_blobs/eng.traineddata b/app/src/main/assets/ocr_blobs/eng.traineddata new file mode 100644 index 000000000000..bbef4675053b Binary files /dev/null and b/app/src/main/assets/ocr_blobs/eng.traineddata differ diff --git a/app/src/main/assets/ocr_blobs/osd.traineddata b/app/src/main/assets/ocr_blobs/osd.traineddata new file mode 100644 index 000000000000..183644aa5754 Binary files /dev/null and b/app/src/main/assets/ocr_blobs/osd.traineddata differ diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index 277469a57375..c3dee63507a9 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -34,6 +34,8 @@ import com.nextcloud.ui.composeActivity.ComposeActivity; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nmc.android.ui.LauncherActivity; +import com.nmc.android.scans.SaveScannedDocumentFragment; +import com.nmc.android.scans.ScanActivity; import com.owncloud.android.MainApp; import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.authentication.DeepLinkLoginActivity; @@ -461,6 +463,12 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract FileUploadHelper fileUploadHelper(); + @ContributesAndroidInjector + abstract ScanActivity scanActivity(); + + @ContributesAndroidInjector + abstract SaveScannedDocumentFragment saveScannedDocumentFragment(); + @ContributesAndroidInjector abstract SslUntrustedCertDialog sslUntrustedCertDialog(); diff --git a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt index ebcb5f7dd4d3..e1cc1c91db4c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt @@ -22,6 +22,7 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.common.NextcloudClient +import com.nmc.android.utils.FileUtils import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.ArbitraryDataProvider @@ -112,6 +113,9 @@ class AccountRemovalWork( preferences.currentAccountName = "" } + //delete the files during logout work from Directory pictures + FileUtils.deleteFilesFromPicturesDirectory(applicationContext) + // remove all files storageManager.removeLocalFiles(user, storageManager) 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 1a8d0df4d294..5bba5da4b4c2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -28,6 +28,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences +import com.nmc.android.jobs.ScanDocUploadWorker import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager @@ -94,6 +95,7 @@ class BackgroundJobFactory @Inject constructor( FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) + ScanDocUploadWorker::class -> createScanDocUploadWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters) @@ -273,6 +275,15 @@ class BackgroundJobFactory @Inject constructor( ) } + private fun createScanDocUploadWork(context: Context, params: WorkerParameters): ScanDocUploadWorker { + return ScanDocUploadWorker( + context = context, + params = params, + notificationManager, + accountManager + ) + } + private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork { return HealthStatusWork( context, 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 9290e2e4e622..84f54ce22cd1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -161,6 +161,13 @@ interface BackgroundJobManager { fun startPdfGenerateAndUploadWork(user: User, uploadFolder: String, imagePaths: List, pdfPath: String) + fun scheduleImmediateScanDocUploadJob( + saveFileTypes: String, + docFileName: String, + remotePathToUpload: String, + pdfPassword: String? + ): LiveData + fun scheduleTestJob() fun startImmediateTestJob() fun cancelTestJob() @@ -174,4 +181,6 @@ interface BackgroundJobManager { fun startPeriodicallyOfflineOperation() fun scheduleInternal2WaySync(intervalMinutes: Long) fun cancelAllFilesDownloadJobs() + + fun isWorkScheduled(tag: String): Boolean } 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 422d5a924c9d..c6e606d5db21 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -21,6 +21,7 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.workDataOf +import com.google.common.util.concurrent.ListenableFuture import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable @@ -33,8 +34,10 @@ import com.nextcloud.utils.extensions.isWorkRunning import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.OCFile import com.owncloud.android.operations.DownloadType +import com.nmc.android.jobs.ScanDocUploadWorker import java.util.Date import java.util.UUID +import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import kotlin.reflect.KClass @@ -83,6 +86,8 @@ internal class BackgroundJobManagerImpl( const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_OFFLINE_OPERATIONS = "offline_operations" const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations" + const val JOB_IMMEDIATE_SCAN_DOC_UPLOAD = "immediate_scan_doc_upload" + const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" @@ -660,6 +665,26 @@ internal class BackgroundJobManagerImpl( workManager.enqueue(request) } + override fun scheduleImmediateScanDocUploadJob( + saveFileTypes: String, docFileName: String, + remotePathToUpload: + String, pdfPassword: String? + ): LiveData { + val data = Data.Builder() + .putString(ScanDocUploadWorker.DATA_REMOTE_PATH, remotePathToUpload) + .putString(ScanDocUploadWorker.DATA_SCAN_FILE_TYPES, saveFileTypes) + .putString(ScanDocUploadWorker.DATA_SCAN_PDF_PWD, pdfPassword) + .putString(ScanDocUploadWorker.DATA_DOC_FILE_NAME, docFileName) + .build() + + val request = oneTimeRequestBuilder(ScanDocUploadWorker::class, JOB_IMMEDIATE_SCAN_DOC_UPLOAD) + .setInputData(data) + .build() + + workManager.enqueueUniqueWork(JOB_IMMEDIATE_SCAN_DOC_UPLOAD, ExistingWorkPolicy.APPEND_OR_REPLACE, request) + return workManager.getJobInfo(request.id) + } + override fun scheduleTestJob() { val request = periodicRequestBuilder(TestJob::class, JOB_TEST) .setInitialDelay(DEFAULT_IMMEDIATE_JOB_DELAY_SEC, TimeUnit.SECONDS) @@ -717,4 +742,21 @@ internal class BackgroundJobManagerImpl( workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request) } + + override fun isWorkScheduled(tag: String): Boolean { + val statuses: ListenableFuture> = workManager.getWorkInfosByTag(tag) + return try { + var running = false + val workInfoList: List = statuses.get() + for (workInfo in workInfoList) { + val state = workInfo.state + running = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED + } + running + } catch (e: ExecutionException) { + false + } catch (e: InterruptedException) { + false + } + } } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index f98bcce0ecd2..8e9d8827c071 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -356,6 +356,15 @@ default void onDarkThemeModeChanged(DarkMode mode) { void setPowerCheckDisabled(boolean value); + /** + * Saves the previously selected storage path to save scanned document + * default value will be Scan folder which will be automatically created first time + * @param path of the folder previously selected + */ + void setUploadScansLastPath(String path); + + String getUploadScansLastPath(); + void increasePinWrongAttempts(); void resetPinWrongAttempts(); diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index bb2850aafd66..11c5866e61c4 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -19,6 +19,7 @@ import com.google.gson.Gson; import com.nextcloud.appReview.AppReviewShownModel; import com.nextcloud.client.account.User; +import com.nmc.android.scans.ScanActivity; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; import com.nextcloud.client.jobs.LogEntry; @@ -80,6 +81,7 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__AUTO_UPLOAD_SPLIT_OUT = "autoUploadEntriesSplitOut"; private static final String PREF__AUTO_UPLOAD_INIT = "autoUploadInit"; private static final String PREF__FOLDER_SORT_ORDER = "folder_sort_order"; + private static final String PREF__UPLOAD_SCANS_LAST_PATH = "upload_scans_last_path"; private static final String PREF__FOLDER_LAYOUT = "folder_layout"; private static final String PREF__LOCK_TIMESTAMP = "lock_timestamp"; @@ -702,6 +704,16 @@ public void setPowerCheckDisabled(boolean value) { preferences.edit().putBoolean(PREF__POWER_CHECK_DISABLED, value).apply(); } + @Override + public void setUploadScansLastPath(String path) { + preferences.edit().putString(PREF__UPLOAD_SCANS_LAST_PATH, path).apply(); + } + + @Override + public String getUploadScansLastPath() { + return preferences.getString(PREF__UPLOAD_SCANS_LAST_PATH, ScanActivity.DEFAULT_UPLOAD_SCAN_PATH); + } + public void increasePinWrongAttempts() { int count = preferences.getInt(PREF__PIN_BRUTE_FORCE_COUNT, 0); preferences.edit().putInt(PREF__PIN_BRUTE_FORCE_COUNT, count + 1).apply(); diff --git a/app/src/main/java/com/nmc/android/adapters/ViewPagerFragmentAdapter.java b/app/src/main/java/com/nmc/android/adapters/ViewPagerFragmentAdapter.java new file mode 100644 index 000000000000..67aab09585e5 --- /dev/null +++ b/app/src/main/java/com/nmc/android/adapters/ViewPagerFragmentAdapter.java @@ -0,0 +1,43 @@ +package com.nmc.android.adapters; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +public class ViewPagerFragmentAdapter extends FragmentStateAdapter { + private final List fragmentList = new ArrayList<>(); + + public ViewPagerFragmentAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + public ViewPagerFragmentAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return fragmentList.get(position); + } + + @Override + public int getItemCount() { + return fragmentList.size(); + } + + public void addFragment(Fragment fragment) { + fragmentList.add(fragment); + } + + public Fragment getFragment(int position){ + if (fragmentList.size() > 0 && position>=0 && fragmentList.size()-1 >= position){ + return fragmentList.get(position); + } + return null; + } +} diff --git a/app/src/main/java/com/nmc/android/interfaces/OnDocScanListener.kt b/app/src/main/java/com/nmc/android/interfaces/OnDocScanListener.kt new file mode 100644 index 000000000000..c128d50a4d7c --- /dev/null +++ b/app/src/main/java/com/nmc/android/interfaces/OnDocScanListener.kt @@ -0,0 +1,16 @@ +package com.nmc.android.interfaces + +import android.graphics.Bitmap + +interface OnDocScanListener { + fun addScannedDoc(file: Bitmap?) + + fun getScannedDocs(): List + + fun removedScannedDoc(file: Bitmap?, index: Int): Boolean + + // isFilterApplied will tell whether the filter is applied to the image or not + fun replaceScannedDoc(index: Int, newFile: Bitmap?, isFilterApplied: Boolean): Bitmap? + + fun replaceFilterIndex(index: Int, filterIndex: Int) +} diff --git a/app/src/main/java/com/nmc/android/interfaces/OnFragmentChangeListener.kt b/app/src/main/java/com/nmc/android/interfaces/OnFragmentChangeListener.kt new file mode 100644 index 000000000000..136dfad292fc --- /dev/null +++ b/app/src/main/java/com/nmc/android/interfaces/OnFragmentChangeListener.kt @@ -0,0 +1,7 @@ +package com.nmc.android.interfaces + +import androidx.fragment.app.Fragment + +interface OnFragmentChangeListener { + fun onReplaceFragment(fragment: Fragment, tag: String, addToBackStack: Boolean) +} diff --git a/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt b/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt new file mode 100644 index 000000000000..ac9294878145 --- /dev/null +++ b/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt @@ -0,0 +1,273 @@ +package com.nmc.android.jobs + +import android.app.NotificationManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.text.TextUtils +import androidx.core.app.NotificationCompat +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nmc.android.scans.SaveScannedDocumentFragment +import com.nmc.android.scans.ScanActivity +import com.nmc.android.utils.FileUtils +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.StringUtils +import io.scanbot.ocr.model.OcrPage +import io.scanbot.pdf.model.PageSize +import io.scanbot.pdf.model.PdfConfig +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.ocr.OpticalCharacterRecognizer +import io.scanbot.sdk.ocr.process.OcrResult +import io.scanbot.sdk.persistence.Page +import io.scanbot.sdk.persistence.PageFileStorage +import io.scanbot.sdk.process.PDFRenderer +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.encryption.AccessPermission +import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.security.SecureRandom + +class ScanDocUploadWorker( + private val context: Context, + params: WorkerParameters, + private val notificationManager: NotificationManager, + private val accountManager: UserAccountManager +) : Worker(context, params) { + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var pdfRenderer: PDFRenderer + private lateinit var pageFileStorage: PageFileStorage + private lateinit var opticalCharacterRecognizer: OpticalCharacterRecognizer + private val savedFiles = mutableListOf() + + companion object { + const val TAG = "ScanDocUploadWorkerJob" + const val DATA_REMOTE_PATH = "data_remote_path" + const val DATA_SCAN_FILE_TYPES = "data_scan_file_types" + const val DATA_SCAN_PDF_PWD = "data_scan_pdf_pwd" + const val DATA_DOC_FILE_NAME = "data_doc_file_name" + const val IMAGE_COMPRESSION_PERCENTAGE = 85 + } + + override fun doWork(): Result { + initScanBotSDK() + val remoteFolderPath = inputData.getString(DATA_REMOTE_PATH) + val scanDocFileTypes = inputData.getString(DATA_SCAN_FILE_TYPES) + val scanDocPdfPwd = inputData.getString(DATA_SCAN_PDF_PWD) + val docFileName = inputData.getString(DATA_DOC_FILE_NAME) + + val fileTypes = StringUtils.convertStringToList(scanDocFileTypes) + val bitmapList: List = ScanActivity.filteredImages + + val randomId = SecureRandom() + val pushNotificationId = randomId.nextInt() + showNotification(pushNotificationId) + + for (type in fileTypes) { + when (type) { + SaveScannedDocumentFragment.SAVE_TYPE_JPG -> { + saveJPGImageFiles(docFileName, bitmapList) + } + + SaveScannedDocumentFragment.SAVE_TYPE_PNG -> { + savePNGImageFiles(docFileName, bitmapList) + } + + SaveScannedDocumentFragment.SAVE_TYPE_PDF -> { + saveNonOCRPDFFile(docFileName, bitmapList, scanDocPdfPwd) + } + + SaveScannedDocumentFragment.SAVE_TYPE_PDF_OCR -> { + savePDFWithOCR(docFileName, bitmapList, scanDocPdfPwd) + } + + SaveScannedDocumentFragment.SAVE_TYPE_TXT -> { + saveTextFile(docFileName, bitmapList) + } + } + } + notificationManager.cancel(pushNotificationId) + + uploadScannedDocs(remoteFolderPath) + + return Result.success() + } + + private fun showNotification(pushNotificationId: Int) { + val notificationBuilder = + NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_IMAGE_SAVE) + .setSmallIcon(R.drawable.notification_icon) + .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon)) + .setColor(context.resources.getColor(R.color.primary, null)) + .setContentTitle(context.resources.getString(R.string.app_name)) + .setContentText(context.resources.getString(R.string.foreground_service_save)) + .setAutoCancel(false) + + notificationManager.notify(pushNotificationId, notificationBuilder.build()) + } + + private fun initScanBotSDK() { + scanbotSDK = ScanbotSDK(context) + pdfRenderer = scanbotSDK.createPdfRenderer() + pageFileStorage = scanbotSDK.createPageFileStorage() + opticalCharacterRecognizer = scanbotSDK.createOcrRecognizer() + } + + private fun saveJPGImageFiles(fileName: String?, bitmapList: List) { + for (i in bitmapList.indices) { + var newFileName = fileName + val bitmap = bitmapList[i] + if (i > 0) { + newFileName += "($i)" + } + + val jpgFile = FileUtils.saveJpgImage(context, bitmap, newFileName, IMAGE_COMPRESSION_PERCENTAGE) + savedFiles.add(jpgFile.path) + } + } + + private fun savePNGImageFiles(fileName: String?, bitmapList: List) { + for (i in bitmapList.indices) { + var newFileName = fileName + val bitmap = bitmapList[i] + if (i > 0) { + newFileName += "($i)" + } + + val pngFile = FileUtils.savePngImage(context, bitmap, newFileName, IMAGE_COMPRESSION_PERCENTAGE) + savedFiles.add(pngFile.path) + } + } + + private fun saveNonOCRPDFFile(fileName: String?, bitmapList: List, pdfPassword: String?) { + + val pageList = getScannedPages(bitmapList) + val pdfFile: File? = + pdfRenderer.renderDocumentFromPages(pageList, PdfConfig.defaultConfig().copy(pageSize = PageSize.A4)) + if (pdfFile != null) { + val renamedFile = File(pdfFile.parent + OCFile.PATH_SEPARATOR + fileName + ".pdf") + if (pdfFile.renameTo(renamedFile)) { + Log_OC.d(TAG, "File successfully renamed") + } + savePdfFile(pdfPassword, renamedFile) + } + } + + /** + * save pdf file if pdf password is set else add it to list + */ + private fun savePdfFile(pdfPassword: String?, renamedFile: File) { + if (!TextUtils.isEmpty(pdfPassword)) { + pdfWithPassword(renamedFile, pdfPassword) + } else { + savedFiles.add(renamedFile.path) + } + } + + private fun pdfWithPassword(pdfFile: File, pdfPassword: String?) { + try { + val document: PDDocument = PDDocument.load(pdfFile) + //Creating access permission object + val ap = AccessPermission() + //Creating StandardProtectionPolicy object + val spp = StandardProtectionPolicy(pdfPassword, pdfPassword, ap) + //Setting the length of the encryption key + spp.encryptionKeyLength = 128 + //Setting the access permissions + spp.permissions = ap + //Protecting the document + document.protect(spp) + + //save the encrypted pdf file + val os = FileOutputStream(pdfFile) + document.save(os) + + //close the document + document.close() + + //add the file to list + savedFiles.add(pdfFile.path) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } + } + + private fun getScannedPages(bitmapList: List): List { + val pageList: MutableList = ArrayList() + for (bitmap in bitmapList) { + val page = Page(pageFileStorage.add(bitmap), listOf(), DetectionStatus.OK) + pageList.add(page) + } + return pageList + } + + private fun savePDFWithOCR(fileName: String?, bitmapList: List, pdfPassword: String?) { + val ocrResult: OcrResult = + opticalCharacterRecognizer.recognizeTextWithPdfFromPages( + getScannedPages(bitmapList), + PdfConfig.defaultConfig().copy(pageSize = PageSize.A4) + ) + /*val ocrPageList: List = ocrResult.ocrPages + if (ocrPageList.isNotEmpty()) { + val ocrText = ocrResult.recognizedText + }*/ + val ocrPDFFile = ocrResult.sandwichedPdfDocumentFile + if (ocrPDFFile != null) { + val renamedFile = File(ocrPDFFile.parent + OCFile.PATH_SEPARATOR + fileName + "_OCR.pdf") + if (ocrPDFFile.renameTo(renamedFile)) { + Log_OC.d(TAG, "OCR File successfully renamed") + } + savePdfFile(pdfPassword, renamedFile) + } + } + + private fun saveTextFile(fileName: String?, bitmapList: List) { + for (i in bitmapList.indices) { + var newFileName = fileName + val bitmap = bitmapList[i] + if (i > 0) { + newFileName += "($i)" + } + val page = Page(pageFileStorage.add(bitmap), emptyList(), DetectionStatus.OK) + val pageList: MutableList = ArrayList() + pageList.add(page) + val ocrResult: OcrResult = opticalCharacterRecognizer.recognizeTextFromPages(pageList) + val ocrPageList: List = ocrResult.ocrPages + if (ocrPageList.isNotEmpty()) { + val ocrText = ocrResult.recognizedText + val txtFile = FileUtils.writeTextToFile(context, ocrText, newFileName) + savedFiles.add(txtFile.path) + } + } + } + + private fun uploadScannedDocs(remotePathBase: String?) { + val remotePaths = savedFiles.map { + remotePathBase + File(it).name + }.toTypedArray() + + FileUploadHelper.instance().uploadNewFiles( + accountManager.user, + savedFiles.toTypedArray(), + remotePaths, + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + false, // do not create parent folder if not existent + UploadFileOperation.CREATED_BY_USER, + false, + false, + NameCollisionPolicy.RENAME + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/marketTracking/TrackingScanInterface.kt b/app/src/main/java/com/nmc/android/marketTracking/TrackingScanInterface.kt new file mode 100644 index 000000000000..3a517a29f458 --- /dev/null +++ b/app/src/main/java/com/nmc/android/marketTracking/TrackingScanInterface.kt @@ -0,0 +1,14 @@ +package com.nmc.android.marketTracking + +import com.nextcloud.client.preferences.AppPreferences + +/** + * interface to track the scanning events from nmc/1867-scanbot branch + * for implementation look nmc/1925-market_tracking branch + * this class will have the declaration for it since it has the tracking SDK's in place + * since we don't have scanning functionality in this branch so to handle the event we have used interface + */ +interface TrackingScanInterface { + + fun sendScanEvent(appPreferences: AppPreferences) +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/scans/CropScannedDocumentFragment.kt b/app/src/main/java/com/nmc/android/scans/CropScannedDocumentFragment.kt new file mode 100644 index 000000000000..f0001771f93c --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/CropScannedDocumentFragment.kt @@ -0,0 +1,348 @@ +package com.nmc.android.scans + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ActivityInfo +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.PointF +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.util.Pair +import android.view.* +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.nmc.android.utils.ScanBotSdkUtils +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentCropScanBinding +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.core.contourdetector.ContourDetector +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.core.contourdetector.Line2D +import io.scanbot.sdk.process.CropOperation +import io.scanbot.sdk.process.ImageProcessor +import java.util.concurrent.Executors +import kotlin.math.absoluteValue + +class CropScannedDocumentFragment : Fragment() { + private lateinit var binding: FragmentCropScanBinding + private lateinit var onFragmentChangeListener: OnFragmentChangeListener + private lateinit var onDocScanListener: OnDocScanListener + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var imageProcessor: ImageProcessor + private lateinit var contourDetector: ContourDetector + + private var scannedDocIndex: Int = -1 + private lateinit var originalBitmap: Bitmap + private var rotationDegrees = 0 + private var polygonPoints: List? = null + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.getInt(ARG_SCANNED_DOC_INDEX)?.let { + scannedDocIndex = it + } + // Fragment locked in portrait screen orientation + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + override fun onAttach(context: Context) { + super.onAttach(context) + run { + try { + onFragmentChangeListener = context as OnFragmentChangeListener + onDocScanListener = context as OnDocScanListener + } catch (ignored: Exception) { + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(true) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(true) + (requireActivity() as ScanActivity).updateActionBarTitleAndHomeButtonByString(resources.getString(R.string.title_crop_scan)) + } + binding = FragmentCropScanBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + contourDetector = scanbotSDK.createContourDetector() + imageProcessor = scanbotSDK.imageProcessor() + + detectDocument() + binding.cropBtnResetBorders.setOnClickListener { + onClickListener(it) + } + addExtraMarginForSwipeGesture() + addMenuHost() + } + + /** + * method to add extra margins for gestured devices + * where user has to swipe left or right to go back from current screen + * this swipe gestures create issue with existing crop gestures + * to avoid that we have added extra margins on left and right for devices + * greater than API level 9+ (Pie) + */ + private fun addExtraMarginForSwipeGesture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (binding.cropPolygonView.layoutParams is ViewGroup.MarginLayoutParams) { + (binding.cropPolygonView.layoutParams as ViewGroup.MarginLayoutParams).setMargins( + resources.getDimensionPixelOffset(R.dimen.standard_margin), 0, + resources.getDimensionPixelOffset(R.dimen.standard_margin), 0 + ) + binding.cropPolygonView.requestLayout() + } + } + } + + private fun onCropDragListener() { + polygonPoints?.let { points -> + var previous = points + binding.cropPolygonView.setEditPolygonDragStateListener { dragging -> + if (dragging) { + previous = ArrayList(binding.cropPolygonView.polygon.map { PointF(it.x, it.y) }) + } else { + if (!isBigEnough(binding.cropPolygonView.polygon)) { + binding.cropPolygonView.polygon = previous + } + } + } + } + } + + private fun onClickListener(view: View) { + when (view.id) { + R.id.crop_btn_reset_borders -> { + if (binding.cropBtnResetBorders.tag.equals(resources.getString(R.string.crop_btn_reset_crop_text))) { + updateButtonText(resources.getString(R.string.crop_btn_detect_doc_text)) + resetCrop() + } else if (binding.cropBtnResetBorders.tag.equals(resources.getString(R.string.crop_btn_detect_doc_text))) { + updateButtonText(resources.getString(R.string.crop_btn_reset_crop_text)) + detectDocument() + } + } + } + } + + private fun updateButtonText(label: String) { + binding.cropBtnResetBorders.tag = label + binding.cropBtnResetBorders.text = label + } + + private fun resetCrop() { + polygonPoints = getResetPolygons() + binding.cropPolygonView.polygon = getResetPolygons() + onCropDragListener() + } + + private fun getResetPolygons(): List { + val polygonList = mutableListOf() + val pointF = PointF(0.0f, 0.0f) + val pointF1 = PointF(1.0f, 0.0f) + val pointF2 = PointF(1.0f, 1.0f) + val pointF3 = PointF(0.0f, 1.0f) + polygonList.add(pointF) + polygonList.add(pointF1) + polygonList.add(pointF2) + polygonList.add(pointF3) + return polygonList + } + + private fun detectDocument() { + InitImageViewTask().executeOnExecutor(Executors.newSingleThreadExecutor()) + } + + @SuppressLint("StaticFieldLeak") + internal inner class InitImageViewTask : AsyncTask() { + private var previewBitmap: Bitmap? = null + + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg params: Void?): InitImageResult { + val scannedDocs = onDocScanListener.getScannedDocs() + // NMC-3614 fix + if (scannedDocs.isEmpty()) { + return InitImageResult(Pair(listOf(), listOf()), listOf()) + } + originalBitmap = scannedDocs[scannedDocIndex] + previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap) + + val result = contourDetector.detect(originalBitmap) + return when (result?.status) { + DetectionStatus.OK, + DetectionStatus.OK_BUT_BAD_ANGLES, + DetectionStatus.OK_BUT_TOO_SMALL, + DetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + val linesPair = Pair(result.horizontalLines, result.verticalLines) + val polygon = result.polygonF + + InitImageResult(linesPair, polygon) + } + + else -> InitImageResult(Pair(listOf(), listOf()), listOf()) + } + } + + @Deprecated("Deprecated in Java") + override fun onPostExecute(initImageResult: InitImageResult) { + binding.cropPolygonView.setImageBitmap(previewBitmap) + binding.magnifier.setupMagnifier(binding.cropPolygonView) + + // set detected polygon and lines into binding.cropPolygonView + polygonPoints = initImageResult.polygon + binding.cropPolygonView.polygon = initImageResult.polygon + binding.cropPolygonView.setLines(initImageResult.linesPair.first, initImageResult.linesPair.second) + + if (initImageResult.polygon.isEmpty()) { + resetCrop() + } else { + onCropDragListener() + } + } + } + + internal inner class InitImageResult(val linesPair: Pair, List>, val polygon: List) + + private fun crop() { + // crop & warp image by selected polygon (editPolygonView.getPolygon()) + val operations = listOf(CropOperation(binding.cropPolygonView.polygon)) + + var documentImage = imageProcessor.processBitmap(originalBitmap, operations, false) + documentImage?.let { + if (rotationDegrees > 0) { + // rotate the final cropped image result based on current rotation value: + val matrix = Matrix() + matrix.postRotate(rotationDegrees.toFloat()) + documentImage = Bitmap.createBitmap(it, 0, 0, it.width, it.height, matrix, true) + } + onDocScanListener.replaceScannedDoc(scannedDocIndex, documentImage, false) + + onFragmentChangeListener.onReplaceFragment( + EditScannedDocumentFragment.newInstance(scannedDocIndex), ScanActivity.FRAGMENT_EDIT_SCAN_TAG, false + ) + } + } + + private fun addMenuHost() { + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.edit_scan, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_save -> { + crop() + true + } + else -> false + } + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + } + + fun getScannedDocIndex(): Int { + return scannedDocIndex + } + + private fun isBigEnough(polygon: List): Boolean { + if (polygon.isEmpty()) { + return true + } + + /* + We receive the array of 4 Polygons when user start dragging the borders to crop the document + 1. polygon[0].x to polygon[3].x --> When user drag from left to right or right to left + 2. polygon[0].y to polygon[3].y --> When user drag from top to bottom or bottom to top + + Now to find the minimum difference we need to compare X and Y polygons. Here we have 2 cases: + 1. For Y polygon: + 1.1. When user dragging from Top to Bottom --> In this case Y will have same value in 0 & 1 index + i.e. polygon[0].y & polygon[1].y + + 1.2. When user dragging from Bottom to Top --> In this case Y will have same value in 2 & 3 index + i.e. polygon[2].y & polygon[3].y + + 2. For X polygon: + 2.1. When user dragging from Left to Right --> In this case X will have same value in 0 & 3 index + i.e. polygon[0].x & polygon[3].x + + 2.2. When user dragging from Right to Left --> In this case X will have same value in 1 & 2 index + i.e. polygon[1].x & polygon[2].x + + + Now to avoid user cropping the whole document we need to have minimum cropping point. To do that + we need to check the difference between the polygon for X and Y like: + 1. For Y: check the difference between polygon[0].y - polygon[2].y and so on + 2. For X: check the difference between polygon[0].x - polygon[1].x and so on + + */ + + if ((polygon[0].y - polygon[2].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[0].y - polygon[3].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[1].y - polygon[2].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[1].y - polygon[3].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[0].x - polygon[1].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[0].x - polygon[2].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[3].x - polygon[1].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[3].x - polygon[2].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + return true + } + + companion object { + private const val ARG_SCANNED_DOC_INDEX = "scanned_doc_index" + + //variable used to avoid cropping the whole document + private const val MINIMUM_CROP_REQUIRED = 0.1 + + @JvmStatic + fun newInstance(index: Int): CropScannedDocumentFragment { + val args = Bundle() + args.putInt(ARG_SCANNED_DOC_INDEX, index) + val fragment = CropScannedDocumentFragment() + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/scans/EditScannedDocumentFragment.kt b/app/src/main/java/com/nmc/android/scans/EditScannedDocumentFragment.kt new file mode 100644 index 000000000000..5b6ab8e4847c --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/EditScannedDocumentFragment.kt @@ -0,0 +1,216 @@ +package com.nmc.android.scans + +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.graphics.Bitmap +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.nmc.android.adapters.ViewPagerFragmentAdapter +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.nmc.android.scans.ScanDocumentFragment.Companion.newInstance +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentEditScannedDocumentBinding + +class EditScannedDocumentFragment : Fragment(), View.OnClickListener { + private lateinit var binding: FragmentEditScannedDocumentBinding + private lateinit var pagerFragmentAdapter: ViewPagerFragmentAdapter + private var onFragmentChangeListener: OnFragmentChangeListener? = null + private var onDocScanListener: OnDocScanListener? = null + + private var selectedScannedDocFile: Bitmap? = null + private var currentSelectedItemIndex = 0 + private var currentItemIndex = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let{ + currentItemIndex = it.getInt(ARG_CURRENT_INDEX, 0) + } + //Fragment screen orientation normal both portrait and landscape + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + + override fun onAttach(context: Context) { + super.onAttach(context) + try { + onFragmentChangeListener = context as OnFragmentChangeListener + onDocScanListener = context as OnDocScanListener + } catch (ignored: Exception) { + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(true) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(true) + (requireActivity() as ScanActivity).updateActionBarTitleAndHomeButtonByString( + resources.getString(R.string.title_edit_scan) + ) + } + binding = FragmentEditScannedDocumentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setUpViewPager() + + binding.cropDocButton.setOnClickListener(this) + binding.scanMoreButton.setOnClickListener(this) + binding.filterDocButton.setOnClickListener(this) + binding.rotateDocButton.setOnClickListener(this) + binding.deleteDocButton.setOnClickListener(this) + + addMenuHost() + } + + private fun setUpViewPager() { + pagerFragmentAdapter = ViewPagerFragmentAdapter(this) + val filesList = onDocScanListener?.getScannedDocs() ?: emptyList() + if (filesList.isEmpty()) { + onScanMore(true) + return + } + for (i in filesList.indices) { + pagerFragmentAdapter.addFragment(ScanPagerFragment.newInstance(i)) + } + binding.editScannedViewPager.adapter = pagerFragmentAdapter + binding.editScannedViewPager.post { binding.editScannedViewPager.setCurrentItem(currentItemIndex, false) } + binding.editScannedViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + currentSelectedItemIndex = position + selectedScannedDocFile = filesList[position] + updateDocCountText(position, filesList.size) + } + }) + + if (filesList.size == 1) { + binding.editScanDocCountLabel.visibility = View.INVISIBLE + } else { + binding.editScanDocCountLabel.visibility = View.VISIBLE + updateDocCountText(currentItemIndex, filesList.size) + } + } + + private fun updateDocCountText(position: Int, totalSize: Int) { + binding.editScanDocCountLabel.text = String.format( + resources.getString(R.string.scanned_doc_count), + position + 1, totalSize + ) + } + + override fun onClick(view: View) { + when (view.id) { + R.id.scanMoreButton -> onScanMore(false) + R.id.cropDocButton -> onFragmentChangeListener?.onReplaceFragment( + CropScannedDocumentFragment.newInstance(currentSelectedItemIndex), + ScanActivity.FRAGMENT_CROP_SCAN_TAG, false + ) + + R.id.filterDocButton -> showFilterDialog() + R.id.rotateDocButton -> { + val fragment = pagerFragmentAdapter.getFragment(currentSelectedItemIndex) + if (fragment is ScanPagerFragment) { + fragment.rotate() + } + } + + R.id.deleteDocButton -> { + val isRemoved = + onDocScanListener?.removedScannedDoc(selectedScannedDocFile, currentSelectedItemIndex) ?: false + if (isRemoved) { + setUpViewPager() + } + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + setUpViewPager() + } + + /** + * check if fragment has to open on + button click or when all scans removed + * + * @param isNoItem + */ + private fun onScanMore(isNoItem: Boolean) { + onFragmentChangeListener?.onReplaceFragment( + newInstance(if (isNoItem) ScanActivity.TAG else TAG), + ScanActivity.FRAGMENT_SCAN_TAG, false + ) + } + + private fun showFilterDialog() { + val fragment = pagerFragmentAdapter.getFragment(currentSelectedItemIndex) + if (fragment is ScanPagerFragment) { + fragment.showApplyFilterDialog() + } + } + + private fun addMenuHost() { + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.edit_scan, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_save -> { + val fragment = pagerFragmentAdapter.getFragment(currentSelectedItemIndex) + if (fragment is ScanPagerFragment) { + // if applying filter is not in process then only show save fragment + if (!fragment.isFilterApplyInProgress) { + saveScannedDocs() + } + } else { + saveScannedDocs() + } + true + } + else -> false + } + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + } + + private fun saveScannedDocs() { + onFragmentChangeListener?.onReplaceFragment( + SaveScannedDocumentFragment.newInstance(), + ScanActivity.FRAGMENT_SAVE_SCAN_TAG, false + ) + } + + companion object { + private const val ARG_CURRENT_INDEX = "current_index" + const val TAG: String = "EditScannedDocumentFragment" + + fun newInstance(currentIndex: Int): EditScannedDocumentFragment { + val args = Bundle() + args.putInt(ARG_CURRENT_INDEX, currentIndex) + val fragment = EditScannedDocumentFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nmc/android/scans/SaveScannedDocumentFragment.kt b/app/src/main/java/com/nmc/android/scans/SaveScannedDocumentFragment.kt new file mode 100644 index 000000000000..9e42fad7e6db --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/SaveScannedDocumentFragment.kt @@ -0,0 +1,375 @@ +package com.nmc.android.scans + +import android.app.Activity +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.text.TextUtils +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager.BadTokenException +import android.view.inputmethod.EditorInfo +import android.widget.CompoundButton +import android.widget.ScrollView +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nmc.android.utils.CheckableThemeUtils.tintCheckbox +import com.nmc.android.utils.CheckableThemeUtils.tintSwitch +import com.nmc.android.utils.FileUtils +import com.nmc.android.utils.KeyboardUtils +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentScanSaveBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.FolderPickerActivity +import com.owncloud.android.utils.DisplayUtils +import javax.inject.Inject + +class SaveScannedDocumentFragment : Fragment(), CompoundButton.OnCheckedChangeListener, Injectable, + View.OnClickListener { + + private lateinit var binding: FragmentScanSaveBinding + + private var isFileNameEditable = false + private var remotePath: String = "/" + private var remoteFilePath: OCFile? = null + + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + + @Inject + lateinit var appPreferences: AppPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Fragment screen orientation normal both portrait and landscape + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + retainInstance = true + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(true) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(true) + (requireActivity() as ScanActivity).updateActionBarTitleAndHomeButtonByString( + resources.getString(R.string.title_save_as) + ) + } + binding = FragmentScanSaveBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViews() + prepareRemotePath() + implementCheckListeners() + implementClickEvent() + } + + private fun implementClickEvent() { + binding.scanSaveFilenameInputEditBtn.setOnClickListener(this) + binding.scanSaveLocationEditBtn.setOnClickListener(this) + binding.saveScanBtnCancel.setOnClickListener(this) + binding.saveScanBtnSave.setOnClickListener(this) + } + + /** + * prepare remote path to save scanned files + */ + private fun prepareRemotePath() { + //check if user has selected scan document from sub folders + //if yes then show that folder in location to save scanned documents + //else check in preferences for last selected path + //if no last path selected available then show default /Scans/ path + + if (requireActivity() is ScanActivity) { + val remoteFile = (requireActivity() as ScanActivity).remoteFile + val remotePath = remoteFile?.remotePath + //remote path should not be null and should not be root path i.e only / + if (remotePath != null && remotePath != OCFile.ROOT_PATH) { + setRemoteFilePath(remoteFile) + return + } + + val lastRemotePath = appPreferences.uploadScansLastPath + //if user coming from Root path and the last saved path is not Scans folder + //then show the Root as scan doc path + if (remotePath == OCFile.ROOT_PATH && lastRemotePath != ScanActivity.DEFAULT_UPLOAD_SCAN_PATH) { + setRemoteFilePath(remoteFile) + return + } + } + + setRemoteFilePath(appPreferences.uploadScansLastPath) + } + + fun setRemoteFilePath(remotePath: String) { + remoteFilePath = OCFile(remotePath) + remoteFilePath?.setFolder() + + updateSaveLocationText() + } + + private fun setRemoteFilePath(remoteFile: OCFile) { + remoteFilePath = remoteFile + + updateSaveLocationText() + } + + private fun initViews() { + binding.scanSaveFilenameInput.setText(FileUtils.scannedFileName()) + tintSwitch(binding.scanSavePdfPasswordSwitch) + tintCheckbox( + binding.scanSaveWithoutTxtRecognitionPdfCheckbox, + binding.scanSaveWithoutTxtRecognitionPngCheckbox, + binding.scanSaveWithoutTxtRecognitionJpgCheckbox, + binding.scanSaveWithTxtRecognitionPdfCheckbox, + binding.scanSaveWithTxtRecognitionTxtCheckbox + ) + binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked = true + binding.scanSavePdfPasswordTextInput.defaultHintTextColor = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_focused), + intArrayOf(android.R.attr.state_focused), + ), + intArrayOf( + Color.GRAY, + resources.getColor(R.color.text_color, null) + ) + ) + } + + private fun implementCheckListeners() { + binding.scanSaveWithoutTxtRecognitionPdfCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithoutTxtRecognitionJpgCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithoutTxtRecognitionPngCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithTxtRecognitionPdfCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithTxtRecognitionTxtCheckbox.setOnCheckedChangeListener(this) + binding.scanSavePdfPasswordSwitch.setOnCheckedChangeListener(this) + + binding.scanSaveFilenameInput.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + enableFileNameEditing() + return@setOnEditorActionListener true + } + false + } + } + + private fun enableDisablePdfPasswordSwitch() { + binding.scanSavePdfPasswordSwitch.isEnabled = + binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked || binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked + if (!binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked && !binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked) { + binding.scanSavePdfPasswordSwitch.isChecked = false + } + } + + private fun showHidePdfPasswordInput(isChecked: Boolean) { + binding.scanSavePdfPasswordTextInput.visibility = if (isChecked) View.VISIBLE else View.GONE + if (isChecked) { + binding.scanSaveNestedScrollView.post { binding.scanSaveNestedScrollView.fullScroll(ScrollView.FOCUS_DOWN) } + } + if (isChecked) { + KeyboardUtils.showSoftKeyboard(requireContext(), binding.scanSavePdfPasswordEt) + } else { + KeyboardUtils.hideKeyboardFrom(requireContext(), binding.scanSavePdfPasswordEt) + } + } + + private fun enableFileNameEditing() { + isFileNameEditable = !isFileNameEditable + binding.scanSaveFilenameInput.isEnabled = isFileNameEditable + if (isFileNameEditable) { + binding.scanSaveFilenameInputEditBtn.setImageResource(R.drawable.ic_tick) + KeyboardUtils.showSoftKeyboard(requireContext(), binding.scanSaveFilenameInput) + binding.scanSaveFilenameInput.setSelection( + binding.scanSaveFilenameInput.text.toString().trim { it <= ' ' }.length + ) + } else { + binding.scanSaveFilenameInputEditBtn.setImageResource(R.drawable.ic_pencil_edit) + KeyboardUtils.hideKeyboardFrom(requireContext(), binding.scanSaveFilenameInput) + } + } + + override fun onClick(view: View) { + when (view.id) { + R.id.scan_save_filename_input_edit_btn -> enableFileNameEditing() + R.id.scan_save_location_edit_btn -> { + val action = Intent(requireActivity(), FolderPickerActivity::class.java) + action.putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION) + action.putExtra(FolderPickerActivity.EXTRA_SHOW_ONLY_FOLDER, true) + action.putExtra(FolderPickerActivity.EXTRA_HIDE_ENCRYPTED_FOLDER, false) + scanDocSavePathResultLauncher.launch(action) + } + + R.id.save_scan_btn_cancel -> requireActivity().onBackPressedDispatcher.onBackPressed() + R.id.save_scan_btn_save -> saveScannedFiles() + } + } + + private fun saveScannedFiles() { + val fileName = binding.scanSaveFilenameInput.text.toString().trim { it <= ' ' } + if (TextUtils.isEmpty(fileName)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty) + return + } + + if (!com.owncloud.android.lib.resources.files.FileUtils.isValidName(fileName)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server) + return + } + + if (!binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked + && !binding.scanSaveWithoutTxtRecognitionJpgCheckbox.isChecked + && !binding.scanSaveWithoutTxtRecognitionPngCheckbox.isChecked + && !binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked + && !binding.scanSaveWithTxtRecognitionTxtCheckbox.isChecked + ) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.scan_save_no_file_select_toast) + return + } + + val fileTypesStringBuilder = StringBuilder() + if (binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_PDF) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithoutTxtRecognitionJpgCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_JPG) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithoutTxtRecognitionPngCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_PNG) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_PDF_OCR) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithTxtRecognitionTxtCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_TXT) + } + val pdfPassword = binding.scanSavePdfPasswordEt.text.toString().trim { it <= ' ' } + if (binding.scanSavePdfPasswordSwitch.isChecked && TextUtils.isEmpty(pdfPassword)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.save_scan_empty_pdf_password) + return + } + + // NMC-3670 + if (requireActivity() is ScanActivity) { + remoteFilePath?.let { + (requireActivity() as ScanActivity).checkEncryption(it) { success -> + if (success) { + showPromptToSave(fileName, fileTypesStringBuilder, pdfPassword) + } + } + } + } + } + + private fun showPromptToSave(fileName: String, fileTypesStringBuilder: StringBuilder, pdfPassword: String) { + try { + val alertDialog = AlertDialog.Builder(requireContext()) + .setMessage(R.string.dialog_save_scan_message) + .setPositiveButton(R.string.dialog_ok) { _: DialogInterface?, _: Int -> + startSaving( + fileName, + fileTypesStringBuilder, pdfPassword + ) + } + .create() + + alertDialog.show() + } catch (e: BadTokenException) { + Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.message) + } + } + + private fun startSaving(fileName: String, fileTypesStringBuilder: StringBuilder, pdfPassword: String) { + //start the save and upload worker + backgroundJobManager.scheduleImmediateScanDocUploadJob( + fileTypesStringBuilder.toString(), + fileName, + remotePath, + pdfPassword + ) + + //save the selected location to save scans in preference + appPreferences.uploadScansLastPath = remotePath + + //send the result back with the selected remote path to open selected remote path + val intent = Intent() + val bundle = Bundle() + bundle.putParcelable(EXTRA_SCAN_DOC_REMOTE_PATH, remoteFilePath) + intent.putExtras(bundle) + requireActivity().setResult(Activity.RESULT_OK, intent) + requireActivity().finish() + } + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + when (buttonView.id) { + R.id.scan_save_without_txt_recognition_pdf_checkbox, R.id.scan_save_with_txt_recognition_pdf_checkbox -> enableDisablePdfPasswordSwitch() + R.id.scan_save_pdf_password_switch -> showHidePdfPasswordInput(isChecked) + } + } + + private fun updateSaveLocationText() { + // to upload the scan docs use remote path + remotePath = remoteFilePath?.remotePath ?: OCFile.ROOT_PATH + + // to show file path use decrypted path + var filePathToShow = remoteFilePath?.decryptedRemotePath ?: OCFile.ROOT_PATH + if (filePathToShow.equals(OCFile.ROOT_PATH, ignoreCase = true)) { + filePathToShow = resources.getString(R.string.scan_save_location_root) + } + binding.scanSaveLocationInput.text = filePathToShow + } + + private var scanDocSavePathResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + // There are no request codes + val data: Intent? = result.data + if (data != null) { + val chosenFolder = FolderPickerActivity.EXTRA_FOLDER?.let { + data.getParcelableArgument(FolderPickerActivity.EXTRA_FOLDER, OCFile::class.java) + } + if (chosenFolder != null) { + remoteFilePath = chosenFolder + updateSaveLocationText() + } + } + } + } + + companion object { + private const val TAG: String = "SaveScannedDocumentFragment" + + fun newInstance(): SaveScannedDocumentFragment { + val args = Bundle() + val fragment = SaveScannedDocumentFragment() + fragment.arguments = args + return fragment + } + + const val SAVE_TYPE_PDF: String = "pdf" + const val SAVE_TYPE_PNG: String = "png" + const val SAVE_TYPE_JPG: String = "jpg" + const val SAVE_TYPE_PDF_OCR: String = "pdf_ocr" + const val SAVE_TYPE_TXT: String = "txt" + + const val EXTRA_SCAN_DOC_REMOTE_PATH: String = "scan_doc_remote_path" + } +} diff --git a/app/src/main/java/com/nmc/android/scans/ScanActivity.kt b/app/src/main/java/com/nmc/android/scans/ScanActivity.kt new file mode 100644 index 000000000000..deeb225af6f0 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanActivity.kt @@ -0,0 +1,301 @@ +package com.nmc.android.scans + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.MenuItem +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.nmc.android.scans.ScanDocumentFragment.Companion.newInstance +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityScanBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.operations.CreateFolderIfNotExistOperation +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment +import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.utils.DisplayUtils +import io.scanbot.sdk.ScanbotSDK + +class ScanActivity : FileActivity(), OnFragmentChangeListener, OnDocScanListener { + private lateinit var binding: ActivityScanBinding + lateinit var scanbotSDK: ScanbotSDK + + var remoteFile: OCFile? = null + private set + + // flag to avoid checking folder existence whenever user goes to save fragment + // we will make it true when the operation finishes first time + private var isFolderCheckOperationFinished = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Inflate and set the layout view + binding = ActivityScanBinding.inflate(layoutInflater) + setContentView(binding.root) + remoteFile = intent.getParcelableArgument(EXTRA_REMOTE_PATH, OCFile::class.java) + originalScannedImages.clear() + filteredImages.clear() + scannedImagesFilterIndex.clear() + initScanbotSDK() + setupToolbar() + setupActionBar() + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + private fun setupActionBar() { + val actionBar = delegate.supportActionBar + actionBar?.let { + it.setBackgroundDrawable(ColorDrawable(resources.getColor(R.color.bg_default, null))) + it.setDisplayHomeAsUpEnabled(true) + viewThemeUtils.files.themeActionBar(this, it, false) + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + createScanFragment(savedInstanceState) + } + + private fun createScanFragment(savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + val scanDocumentFragment = newInstance(TAG) + onReplaceFragment(scanDocumentFragment, FRAGMENT_SCAN_TAG, false) + } else { + supportFragmentManager.findFragmentByTag(FRAGMENT_SCAN_TAG) + } + } + + override fun onReplaceFragment(fragment: Fragment, tag: String, addToBackStack: Boolean) { + // only during replacing save scan fragment + if (tag.equals(FRAGMENT_SAVE_SCAN_TAG, ignoreCase = true)) { + checkAndCreateFolderIfRequired() + } + + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.scan_frame_container, fragment, tag) + if (addToBackStack) { + transaction.addToBackStack(tag) + } + transaction.commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressHandle() + } + return super.onOptionsItemSelected(item) + } + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onBackPressHandle() + } + } + + private fun onBackPressHandle() { + val editScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_EDIT_SCAN_TAG) + val cropScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_CROP_SCAN_TAG) + val saveScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_SAVE_SCAN_TAG) + if (cropScanFragment != null || saveScanFragment != null) { + var index = 0 + if (cropScanFragment is CropScannedDocumentFragment) { + index = cropScanFragment.getScannedDocIndex() + } + onReplaceFragment(EditScannedDocumentFragment.newInstance(index), FRAGMENT_EDIT_SCAN_TAG, false) + } else if (editScanFragment != null) { + createScanFragment(null) + } else { + finish() + } + } + + private fun initScanbotSDK() { + scanbotSDK = ScanbotSDK(this) + } + + override fun addScannedDoc(file: Bitmap?) { + file?.let { + originalScannedImages.add(it) + filteredImages.add(it) + scannedImagesFilterIndex.add(0) // no filter by default + } + } + + override fun getScannedDocs(): List { + return filteredImages + } + + override fun removedScannedDoc(file: Bitmap?, index: Int): Boolean { + //removed the filter applied index also when scanned document is removed + if (scannedImagesFilterIndex.size > 0 && scannedImagesFilterIndex.size > index) { + scannedImagesFilterIndex.removeAt(index) + } + if (originalScannedImages.size > 0 && file != null) { + originalScannedImages.removeAt(index) + } + if (filteredImages.size > 0 && file != null) { + filteredImages.removeAt(index) + return true + } + return false + } + + override fun replaceScannedDoc(index: Int, newFile: Bitmap?, isFilterApplied: Boolean): Bitmap? { + //only update the original bitmap if no filter is applied + if (!isFilterApplied && originalScannedImages.size > 0 && newFile != null && index >= 0 && originalScannedImages.size - 1 >= index) { + originalScannedImages[index] = newFile + } + if (filteredImages.size > 0 && newFile != null && index >= 0 && filteredImages.size - 1 >= index) { + return filteredImages.set(index, newFile) + } + return null + } + + override fun replaceFilterIndex(index: Int, filterIndex: Int) { + if (scannedImagesFilterIndex.size > 0 && scannedImagesFilterIndex.size > index) { + scannedImagesFilterIndex[index] = filterIndex + } + } + + private fun checkAndCreateFolderIfRequired() { + val remotePath = remoteFile?.remotePath + + //if user is coming from sub-folder then we should not check for existence as folder will be available + if (!TextUtils.isEmpty(remotePath) && remotePath != OCFile.ROOT_PATH) { + return + } + + //no need to do any operation if its already finished earlier + if (isFolderCheckOperationFinished) { + return + } + + val lastRemotePath = appPreferences.uploadScansLastPath + + //create the default scan folder if it doesn't exist or if user has not selected any other folder + if (lastRemotePath.equals(DEFAULT_UPLOAD_SCAN_PATH, ignoreCase = true)) { + fileOperationsHelper.createFolderIfNotExist(lastRemotePath, false) + return + } + + //if last saved remote path is not root path then we have to check if the folder exist or not + if (lastRemotePath != OCFile.ROOT_PATH) { + fileOperationsHelper.createFolderIfNotExist(lastRemotePath, true) + } + } + + // NMC-3670 + // check if selected folder is encrypted and e2ee is configured or not + fun checkEncryption(file: OCFile, resultListener: (success: Boolean) -> Unit) { + // get file from storage to have the encrypted information + // as we are making OCFile without the flag {see-> SaveScannedDocumentFragment.setRemoteFilePath()} + var remoteFile = storageManager.getFileByEncryptedRemotePath(file.remotePath) + // there can be case where the remoteFile can be null + if (remoteFile == null) { + remoteFile = file + } + + if (!remoteFile.isEncrypted) { + resultListener(true) + return + } + + if (remoteFile.isEncrypted) { + val user = user.orElseThrow { RuntimeException() } + + // check if e2e app is enabled + val ocCapability: OCCapability = storageManager + .getCapability(user.accountName) + + if (ocCapability.endToEndEncryption.isFalse || + ocCapability.endToEndEncryption.isUnknown + ) { + DisplayUtils.showSnackMessage(this, R.string.end_to_end_encryption_not_enabled) + resultListener(false) + return + } + // check if keys are stored + if (FileOperationsHelper.isEndToEndEncryptionSetup(this, user)) { + resultListener(true) + } else { + val setupEncryptionDialogFragment = SetupEncryptionDialogFragment.newInstance(user, -1) + supportFragmentManager.setFragmentResultListener( + SetupEncryptionDialogFragment.RESULT_REQUEST_KEY, + this + ) { requestKey, result -> + if (requestKey == SetupEncryptionDialogFragment.RESULT_REQUEST_KEY) { + resultListener( + !result.getBoolean(SetupEncryptionDialogFragment.RESULT_KEY_CANCELLED, false) + && result.getBoolean(SetupEncryptionDialogFragment.SUCCESS, false) + ) + } + } + setupEncryptionDialogFragment.show( + supportFragmentManager, + SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG + ) + } + } + } + + override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { + super.onRemoteOperationFinish(operation, result) + if (operation is CreateFolderIfNotExistOperation) { + //we are only handling callback when we are checking if folder exist or not to update the UI + //in case the folder doesn't exist (user has deleted) + if (!result.isSuccess && result.code == RemoteOperationResult.ResultCode.FILE_NOT_FOUND) { + val saveScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_SAVE_SCAN_TAG) + if (saveScanFragment != null && saveScanFragment.isVisible) { + //update the root path in preferences as well + //so that next time folder issue won't come + appPreferences.uploadScansLastPath = OCFile.ROOT_PATH + //if folder doesn't exist then we have to set the remote path as root i.e. fallback mechanism + (saveScanFragment as SaveScannedDocumentFragment).setRemoteFilePath(OCFile.ROOT_PATH) + } + } + isFolderCheckOperationFinished = true + } + } + + companion object { + const val FRAGMENT_SCAN_TAG: String = "SCAN_FRAGMENT_TAG" + const val FRAGMENT_EDIT_SCAN_TAG: String = "EDIT_SCAN_FRAGMENT_TAG" + const val FRAGMENT_CROP_SCAN_TAG: String = "CROP_SCAN_FRAGMENT_TAG" + const val FRAGMENT_SAVE_SCAN_TAG: String = "SAVE_SCAN_FRAGMENT_TAG" + + // default path to upload the scanned document + // if user doesn't select any location then this will be the default location + const val DEFAULT_UPLOAD_SCAN_PATH: String = OCFile.ROOT_PATH + "Scans" + OCFile.PATH_SEPARATOR + + const val TAG: String = "ScanActivity" + private const val EXTRA_REMOTE_PATH = "com.nmc.android.scans.scan_activity.extras.remote_path" + + @JvmField + val originalScannedImages: MutableList = ArrayList() //list with original bitmaps + + @JvmField + val filteredImages: MutableList = ArrayList() //list with bitmaps applied filters + + @JvmField + val scannedImagesFilterIndex: MutableList = ArrayList() //list to maintain the state of + // applied filter index when device rotated + + @JvmStatic + fun openScanActivity(context: Context, remoteFile: OCFile, requestCode: Int) { + val intent = Intent(context, ScanActivity::class.java) + intent.putExtra(EXTRA_REMOTE_PATH, remoteFile) + (context as AppCompatActivity).startActivityForResult(intent, requestCode) + } + } +} diff --git a/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt b/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt new file mode 100644 index 000000000000..6f7214dba000 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt @@ -0,0 +1,438 @@ +package com.nmc.android.scans + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.Fragment +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentScanDocumentBinding +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.camera.CaptureInfo +import io.scanbot.sdk.camera.FrameHandlerResult +import io.scanbot.sdk.contourdetector.ContourDetectorFrameHandler +import io.scanbot.sdk.core.contourdetector.ContourDetector +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.docdetection.ui.IDocumentScannerViewCallback +import io.scanbot.sdk.docprocessing.PageProcessor +import io.scanbot.sdk.ocr.OpticalCharacterRecognizer +import io.scanbot.sdk.persistence.PageFileStorage +import io.scanbot.sdk.process.CropOperation +import io.scanbot.sdk.process.ImageProcessor +import io.scanbot.sdk.ui.camera.CameraUiSettings +import io.scanbot.sdk.ui.view.base.configuration.CameraOrientationMode + +class ScanDocumentFragment : Fragment() { + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var contourDetector: ContourDetector + private lateinit var imageProcessor: ImageProcessor + + private var lastUserGuidanceHintTs = 0L + private var flashEnabled = false + private var autoSnappingEnabled = true + private val ignoreBadAspectRatio = true + + //OCR + private lateinit var opticalCharacterRecognizer: OpticalCharacterRecognizer + private lateinit var pageFileStorage: PageFileStorage + private lateinit var pageProcessor: PageProcessor + + private lateinit var onDocScanListener: OnDocScanListener + private lateinit var onFragmentChangeListener: OnFragmentChangeListener + + private lateinit var calledFrom: String + + private lateinit var binding: FragmentScanDocumentBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.getString(ARG_CALLED_FROM)?.let { + calledFrom = it + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + try { + onDocScanListener = context as OnDocScanListener + onFragmentChangeListener = context as OnFragmentChangeListener + } catch (ignored: Exception) { + + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(false) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(false) + } + binding = FragmentScanDocumentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + askPermission() + initDependencies() + + binding.camera.apply { + initCamera(CameraUiSettings(true)) + initDetectionBehavior(contourDetector, + { result -> + // Here you are continuously notified about contour detection results. + // For example, you can show a user guidance text depending on the current detection status. + // don't update the text if fragment is removing + if (!isRemoving) { + // Here you are continuously notified about contour detection results. + // For example, you can show a user guidance text depending on the current detection status. + binding.userGuidanceHint.post { + if (result is FrameHandlerResult.Success<*>) { + showUserGuidance((result as FrameHandlerResult.Success).value.detectionStatus) + } + } + } + false // typically you need to return false + }, + object : IDocumentScannerViewCallback { + override fun onCameraOpen() { + // In this example we demonstrate how to lock the orientation of the UI (Activity) + // as well as the orientation of the taken picture to portrait. + binding.camera.cameraConfiguration.setCameraOrientationMode(CameraOrientationMode.PORTRAIT) + + binding.camera.viewController.useFlash(flashEnabled) + binding.camera.viewController.continuousFocus() + } + + override fun onPictureTaken(image: ByteArray, captureInfo: CaptureInfo) { + processPictureTaken(image, captureInfo.imageOrientation) + + // continue scanning + /*binding.camera.postDelayed({ + binding.camera.viewController.startPreview() + }, 1000)*/ + } + } + ) + + // See https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/using-scanbot-camera-view/#preview-mode + // cameraConfiguration.setCameraPreviewMode(io.scanbot.sdk.camera.CameraPreviewMode.FIT_IN) + } + + binding.camera.viewController.apply { + setAcceptedAngleScore(60.0) + setAcceptedSizeScore(75.0) + setIgnoreBadAspectRatio(ignoreBadAspectRatio) + + // Please note: https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/autosnapping/#sensitivity + setAutoSnappingSensitivity(0.85f) + } + + binding.shutterButton.setOnClickListener { binding.camera.viewController.takePicture(false) } + binding.shutterButton.visibility = View.VISIBLE + + binding.scanDocBtnFlash.setOnClickListener { + flashEnabled = !flashEnabled + binding.camera.viewController.useFlash(flashEnabled) + toggleFlashButtonUI() + } + binding.scanDocBtnCancel.setOnClickListener { + // if fragment opened from Edit Scan Fragment then on cancel click it should go to that fragment + if (calledFrom == EditScannedDocumentFragment.TAG) { + openEditScanFragment() + } else { + // else default behaviour + (requireActivity() as ScanActivity).onBackPressed() + } + } + + binding.scanDocBtnAutomatic.setOnClickListener { + autoSnappingEnabled = !autoSnappingEnabled + setAutoSnapEnabled(autoSnappingEnabled) + } + binding.scanDocBtnAutomatic.post { setAutoSnapEnabled(autoSnappingEnabled) } + + toggleFlashButtonUI() + } + + private fun toggleFlashButtonUI() { + if (flashEnabled) { + binding.scanDocBtnFlash.setIconTintResource(R.color.primary) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) + } else { + binding.scanDocBtnFlash.setIconTintResource(R.color.grey_60) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) + } + } + + private fun askPermission() { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + requestMultiplePermissions.launch( + arrayOf( + Manifest.permission.CAMERA, + ) + ) + } + } + + private fun initDependencies() { + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + contourDetector = scanbotSDK.createContourDetector() + imageProcessor = scanbotSDK.imageProcessor() + opticalCharacterRecognizer = scanbotSDK.createOcrRecognizer() + pageFileStorage = scanbotSDK.createPageFileStorage() + pageProcessor = scanbotSDK.createPageProcessor() + } + + override fun onResume() { + super.onResume() + binding.camera.viewController.onResume() + binding.scanDocProgressBar.visibility = View.GONE + } + + override fun onPause() { + super.onPause() + binding.camera.viewController.onPause() + } + + private fun showUserGuidance(result: DetectionStatus) { + if (!autoSnappingEnabled) { + return + } + if (System.currentTimeMillis() - lastUserGuidanceHintTs < 400) { + return + } + + // Make sure to reset the default polygon fill color (see the ignoreBadAspectRatio case). + // polygonView.setFillColor(POLYGON_FILL_COLOR) + // fragment should be added and visible because this method is being called from handler + // it can be called when fragment is not attached or visible + if (isAdded && isVisible) { + when (result) { + DetectionStatus.OK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.OK_BUT_TOO_SMALL -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_move_closer) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.OK_BUT_BAD_ANGLES -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_perspective) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.ERROR_NOTHING_DETECTED -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_no_doc) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.ERROR_TOO_NOISY -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_bg_noisy) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + if (ignoreBadAspectRatio) { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + // change polygon color to "OK" + // polygonView.setFillColor(POLYGON_FILL_COLOR_OK) + } else { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_aspect_ratio) + } + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.ERROR_TOO_DARK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_poor_light) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + else -> binding.userGuidanceHint.visibility = View.GONE + } + } + lastUserGuidanceHintTs = System.currentTimeMillis() + } + + private fun processPictureTaken(image: ByteArray, imageOrientation: Int) { + requireActivity().runOnUiThread { + binding.camera.viewController.onPause() + binding.scanDocProgressBar.visibility = View.VISIBLE + //cameraView.visibility = View.GONE + } + // Here we get the full image from the camera. + // Please see https://github.com/doo/Scanbot-SDK-Examples/wiki/Handling-camera-picture + // This is just a demo showing the detected document image as a downscaled(!) preview image. + + // Decode Bitmap from bytes of original image: + val options = BitmapFactory.Options() + // Please note: In this simple demo we downscale the original image to 1/8 for the preview! + //options.inSampleSize = 8 + // Typically you will need the full resolution of the original image! So please change the "inSampleSize" value to 1! + options.inSampleSize = 1 + var originalBitmap = BitmapFactory.decodeByteArray(image, 0, image.size, options) + + // Rotate the original image based on the imageOrientation value. + // Required for some Android devices like Samsung! + if (imageOrientation > 0) { + val matrix = Matrix() + matrix.setRotate(imageOrientation.toFloat(), originalBitmap.width / 2f, originalBitmap.height / 2f) + originalBitmap = Bitmap.createBitmap( + originalBitmap, + 0, + 0, + originalBitmap.width, + originalBitmap.height, + matrix, + false + ) + } + + // Run document detection on original image: + val result = contourDetector.detect(originalBitmap)!! + val detectedPolygon = result.polygonF + + val documentImage = imageProcessor.processBitmap(originalBitmap, CropOperation(detectedPolygon), false) + + // val file = saveImage(documentImage) + // Log.d("SCANNING","File : $file") + if (documentImage != null) { + onDocScanListener.addScannedDoc(documentImage) + // onDocScanListener.addScannedDoc(FileUtils.saveImage(requireContext(), documentImage, null)) + openEditScanFragment() + + /* uiScope.launch { + recognizeTextWithoutPDFTask(documentImage) + }*/ + } + // RecognizeTextWithoutPDFTask(documentImage).execute() + + //resultView.post { resultView.setImageBitmap(documentImage) } + + // continue scanning + /* cameraView.postDelayed({ + cameraView.continuousFocus() + cameraView.startPreview() + }, 1000)*/ + } + + private fun openEditScanFragment() { + onFragmentChangeListener.onReplaceFragment( + EditScannedDocumentFragment.newInstance(onDocScanListener.getScannedDocs().size - 1), + ScanActivity.FRAGMENT_EDIT_SCAN_TAG, false + ) + } + + private fun setAutoSnapEnabled(enabled: Boolean) { + binding.camera.viewController.apply { + autoSnappingEnabled = enabled + isFrameProcessingEnabled = enabled + } + binding.polygonView.visibility = if (enabled) View.VISIBLE else View.GONE + /*autoSnappingToggleButton.text = resources.getString(R.string.automatic) + " ${ + if (enabled) "ON" else + "OFF" + }"*/ + if (enabled) { + binding.scanDocBtnAutomatic.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) + binding.shutterButton.showAutoButton() + } else { + binding.scanDocBtnAutomatic.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) + binding.shutterButton.showManualButton() + binding.userGuidanceHint.visibility = View.GONE + } + } + + private val requestMultiplePermissions = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + permissions.entries.forEach { + if (!it.value) { + // permission not granted + val showRationale = shouldShowRequestPermissionRationale(it.key) + if (!showRationale) { + // user also CHECKED "never ask again" + // you can either enable some fall back, + // disable features of your app + // or open another dialog explaining + // again the permission and directing to + // the app setting + onPermissionDenied(requireActivity().resources.getString(R.string.camera_permission_rationale)) + } else if (Manifest.permission.CAMERA == it.key) { + // user did NOT check "never ask again" + // this is a good place to explain the user + // why you need the permission and ask if he wants + // to accept it (the rationale) + onPermissionDenied(requireActivity().resources.getString(R.string.camera_permission_denied)) + + // askPermission() + } + // else if ( /* possibly check more permissions...*/) { + // } + } + } + } + + private fun onPermissionDenied(message: String) { + // Show Toast instead of snackbar as we are finishing the activity + Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show() + requireActivity().finish() + } + + companion object { + + @JvmStatic + val ARG_CALLED_FROM = "arg_called_From" + + @JvmStatic + fun newInstance(calledFrom: String): ScanDocumentFragment { + val args = Bundle() + args.putString(ARG_CALLED_FROM, calledFrom) + val fragment = ScanDocumentFragment() + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/scans/ScanPagerFragment.kt b/app/src/main/java/com/nmc/android/scans/ScanPagerFragment.kt new file mode 100644 index 000000000000..284afa096819 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanPagerFragment.kt @@ -0,0 +1,219 @@ +package com.nmc.android.scans + +import android.content.Context +import android.content.DialogInterface +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.os.HandlerCompat +import androidx.fragment.app.Fragment +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.utils.ScanBotSdkUtils.resizeForPreview +import com.owncloud.android.R +import com.owncloud.android.databinding.ItemScannedDocBinding +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.process.FilterOperation +import io.scanbot.sdk.process.ImageFilterType +import io.scanbot.sdk.process.RotateOperation +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class ScanPagerFragment : Fragment() { + private lateinit var binding: ItemScannedDocBinding + + private lateinit var scanbotSDK: ScanbotSDK + private var originalBitmap: Bitmap? = null + private var previewBitmap: Bitmap? = null + + private val executorService: ExecutorService = Executors.newSingleThreadExecutor() + private val handler = HandlerCompat.createAsync(Looper.getMainLooper()) + + private var lastRotationEventTs = 0L + private var rotationDegrees = 0 + private var index = 0 + + private var onDocScanListener: OnDocScanListener? = null + private var applyFilterDialog: AlertDialog? = null + private var selectedFilter = 0 + + // scan should not be saved till filter is applied + // flag to check if applying filter is in progress or not + var isFilterApplyInProgress: Boolean = false + private set + + override fun onAttach(context: Context) { + super.onAttach(context) + try { + onDocScanListener = context as OnDocScanListener + } catch (ignored: Exception) { + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let{ + index = it.getInt(ARG_SCANNED_DOC_PATH) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (requireActivity() is ScanActivity) { + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + } + binding = ItemScannedDocBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //File file = new File(scannedDocPath); + //originalBitmap = FileUtils.convertFileToBitmap(file); + // previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap); + // loadImage(); + setUpBitmap() + } + + private fun setUpBitmap() { + executorService.execute { + if (index >= 0 && index < ScanActivity.filteredImages.size) { + originalBitmap = onDocScanListener?.getScannedDocs()?.get(index) + originalBitmap?.let { + previewBitmap = resizeForPreview(it) + } + } + if (index >= 0 && index < ScanActivity.scannedImagesFilterIndex.size) { + selectedFilter = ScanActivity.scannedImagesFilterIndex[index] + } + handler.post { loadImage() } + } + } + + private fun loadImage() { + if (this::binding.isInitialized) { + if (previewBitmap != null) { + binding.editScannedImageView.setImageBitmap(previewBitmap) + } else if (originalBitmap != null) { + binding.editScannedImageView.setImageBitmap(originalBitmap) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + applyFilterDialog?.let { + if (it.isShowing) { + it.dismiss() + } + } + } + + fun rotate() { + if (System.currentTimeMillis() - lastRotationEventTs < 350) { + return + } + rotationDegrees += 90 + binding.editScannedImageView.rotateClockwise() + lastRotationEventTs = System.currentTimeMillis() + originalBitmap?.let{ + executorService.execute { + val rotatedBitmap = scanbotSDK.imageProcessor().processBitmap( + it, + ArrayList(listOf(RotateOperation(rotationDegrees))), false + ) + onDocScanListener?.replaceScannedDoc(index, rotatedBitmap, false) + } + } + } + + fun showApplyFilterDialog() { + val filterArray = resources.getStringArray(R.array.edit_scan_filter_values) + val builder = AlertDialog.Builder(requireActivity()) + builder.setTitle(R.string.edit_scan_filter_dialog_title) + .setSingleChoiceItems( + filterArray, + selectedFilter + ) { dialog: DialogInterface, which: Int -> + selectedFilter = which + onDocScanListener?.replaceFilterIndex(index, selectedFilter) + if (filterArray[which].equals(resources.getString(R.string.edit_scan_filter_none), ignoreCase = true)) { + applyFilter(ImageFilterType.NONE) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_pure_binarized), + ignoreCase = true + ) + ) { + applyFilter(ImageFilterType.PURE_BINARIZED) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_color_enhanced), + ignoreCase = true + ) + ) { + applyFilter(ImageFilterType.COLOR_ENHANCED, ImageFilterType.EDGE_HIGHLIGHT) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_color_document), + ignoreCase = true + ) + ) { + applyFilter(ImageFilterType.COLOR_DOCUMENT) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_grey), + ignoreCase = true + ) + ) { + applyFilter(ImageFilterType.GRAYSCALE) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_b_n_w), + ignoreCase = true + ) + ) { + applyFilter(ImageFilterType.BLACK_AND_WHITE) + } + dialog.dismiss() + } + .setOnCancelListener { } + applyFilterDialog = builder.create() + applyFilterDialog?.show() + } + + private fun applyFilter(vararg imageFilterType: ImageFilterType) { + binding.editScanImageProgressBar.visibility = View.VISIBLE + isFilterApplyInProgress = true + originalBitmap?.let { + executorService.execute { + if (imageFilterType[0] != ImageFilterType.NONE) { + val filterOperationList: MutableList = ArrayList() + for (filters in imageFilterType) { + filterOperationList.add(FilterOperation(filters)) + } + previewBitmap = + scanbotSDK.imageProcessor().processBitmap(it, filterOperationList, false) + } else { + previewBitmap = ScanActivity.originalScannedImages[index] + } + onDocScanListener?.replaceScannedDoc(index, previewBitmap, true) + handler.post { + isFilterApplyInProgress = false + binding.editScanImageProgressBar.visibility = View.GONE + loadImage() + } + } + } + } + + companion object { + private const val ARG_SCANNED_DOC_PATH = "scanned_doc_path" + + fun newInstance(i: Int): ScanPagerFragment { + val args = Bundle() + args.putInt(ARG_SCANNED_DOC_PATH, i) + + val fragment = ScanPagerFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt new file mode 100644 index 000000000000..a3b8a1149948 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt @@ -0,0 +1,117 @@ +package com.nmc.android.utils + +import android.content.res.ColorStateList +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.res.ResourcesCompat +import com.owncloud.android.R + +object CheckableThemeUtils { + @JvmStatic + fun tintCheckbox(vararg checkBoxes: AppCompatCheckBox) { + for (checkBox in checkBoxes) { + val checkEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_enabled, + checkBox.context.theme + ) + val checkDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_disabled, + checkBox.context.theme + ) + val uncheckEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_enabled, + checkBox.context.theme + ) + val uncheckDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_disabled, + checkBox.context.theme + ) + + val states = arrayOf( + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled, -android.R.attr.state_checked) + ) + val colors = intArrayOf( + checkEnabled, + checkDisabled, + uncheckEnabled, + uncheckDisabled + ) + checkBox.buttonTintList = ColorStateList(states, colors) + } + } + + @JvmStatic + @JvmOverloads + fun tintSwitch(switchView: SwitchCompat, color: Int = 0, colorText: Boolean = false) { + if (colorText) { + switchView.setTextColor(color) + } + + val states = arrayOf( + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled) + ) + + val thumbColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_checked_enabled, + switchView.context.theme + ) + val thumbColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_unchecked_enabled, + switchView.context.theme + ) + val thumbColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_disabled, + switchView.context.theme + ) + + val thumbColors = intArrayOf( + thumbColorCheckedEnabled, + thumbColorUncheckedEnabled, + thumbColorDisabled + ) + val thumbColorStateList = ColorStateList(states, thumbColors) + + val trackColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_checked_enabled, + switchView.context.theme + ) + val trackColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_unchecked_enabled, + switchView.context.theme + ) + val trackColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_disabled, + switchView.context.theme + ) + + val trackColors = intArrayOf( + trackColorCheckedEnabled, + trackColorUncheckedEnabled, + trackColorDisabled + ) + + val trackColorStateList = ColorStateList(states, trackColors) + + switchView.thumbTintList = thumbColorStateList + switchView.trackTintList = trackColorStateList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/FileUtils.java b/app/src/main/java/com/nmc/android/utils/FileUtils.java new file mode 100644 index 000000000000..a161a2acbdb9 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/FileUtils.java @@ -0,0 +1,171 @@ +package com.nmc.android.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.text.TextUtils; + +import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.ui.helpers.FileOperationsHelper; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import androidx.annotation.NonNull; + +// TODO: 06/24/23 Migrate to FileUtil once Rotate PR is upstreamed and merged by NC +public class FileUtils { + private static final String TAG = FileUtils.class.getSimpleName(); + + private static final String SCANS_FILE_DIR = "Scans"; + private static final String SCANNED_FILE_PREFIX = "scan_"; + + // while generating pdf using Scanbot it provide us following path: + // /scanbot-sdk/snapping_documents/.pdf + // this path will help us to differentiate if pdf file is generating by scanbot + private static final String SCANBOT_PDF_LOCAL_PATH = "/scanbot-sdk/snapping_documents/"; + private static final int JPG_FILE_TYPE = 1; + private static final int PNG_FILE_TYPE = 2; + + public static File saveJpgImage(Context context, Bitmap bitmap, String imageName, int quality) { + return createFileAndSaveImage(context, bitmap, imageName, quality, JPG_FILE_TYPE); + } + + public static File savePngImage(Context context, Bitmap bitmap, String imageName, int quality) { + return createFileAndSaveImage(context, bitmap, imageName, quality, PNG_FILE_TYPE); + } + + private static File createFileAndSaveImage(Context context, Bitmap bitmap, String imageName, int quality, + int fileType) { + File file = fileType == PNG_FILE_TYPE ? getPngImageName(context, imageName) : getJpgImageName(context, + imageName); + return saveImage(file, bitmap, quality, fileType); + } + + private static File saveImage(File file, Bitmap bitmap, int quality, int fileType) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos); + byte[] bitmapData = bos.toByteArray(); + + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(bitmapData); + fileOutputStream.flush(); + fileOutputStream.close(); + return file; + } catch (Exception e) { + Log_OC.e(TAG, " Failed to save image : " + e.getLocalizedMessage()); + return null; + } + } + + private static File getJpgImageName(Context context, String imageName) { + File imageFile = getOutputMediaFile(context); + if (!TextUtils.isEmpty(imageName)) { + return new File(imageFile.getPath() + File.separator + imageName + ".jpg"); + } else { + return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName()); + } + } + + private static File getPngImageName(Context context, String imageName) { + File imageFile = getOutputMediaFile(context); + if (!TextUtils.isEmpty(imageName)) { + return new File(imageFile.getPath() + File.separator + imageName + ".png"); + } else { + return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".png")); + } + } + + private static File getTextFileName(Context context, String fileName) { + File txtFileName = getOutputMediaFile(context); + if (!TextUtils.isEmpty(fileName)) { + return new File(txtFileName.getPath() + File.separator + fileName + ".txt"); + } else { + return new File(txtFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".txt")); + } + } + + private static File getPdfFileName(Context context, String fileName) { + File pdfFileName = getOutputMediaFile(context); + if (!TextUtils.isEmpty(fileName)) { + return new File(pdfFileName.getPath() + File.separator + fileName + ".pdf"); + } else { + return new File(pdfFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".pdf", ".txt")); + } + } + + public static String scannedFileName() { + return SCANNED_FILE_PREFIX + new SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US).format(new Date()); + } + + public static File getOutputMediaFile(Context context) { + File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), SCANS_FILE_DIR); + if (!file.exists()) { + file.mkdir(); + } + return file; + } + + public static Bitmap convertFileToBitmap(File file) { + String filePath = file.getPath(); + Bitmap bitmap = BitmapFactory.decodeFile(filePath); + return bitmap; + } + + public static File writeTextToFile(Context context, String textToWrite, String fileName) { + File file = getTextFileName(context, fileName); + try { + FileWriter fileWriter = new FileWriter(file); + fileWriter.write(textToWrite); + fileWriter.flush(); + fileWriter.close(); + return file; + } catch (IOException e) { + //e.printStackTrace(); + Log_OC.e(TAG, "Failed to write file : " + e.toString()); + } + return null; + + } + + /** + * method to check if uploading file is from Scans or not + * + * @param path local path of the uploading file + */ + public static boolean isScannedFiles(@NonNull Context context, @NonNull String path) { + if (path.isEmpty()) { + return false; + } + + return (path.contains(getOutputMediaFile(context).getPath()) || path.contains(SCANBOT_PDF_LOCAL_PATH)); + } + + /** + * delete all the files inside the pictures directory + * this directory is getting used to store the scanned images temporarily till they uploaded to cloud + * the scanned files after downloading will get deleted by UploadWorker but in case some files still there + * then we have to delete it when user do logout from the app + * @param context + */ + public static void deleteFilesFromPicturesDirectory(Context context) { + File getFileDirectory = getOutputMediaFile(context); + if (getFileDirectory.isDirectory()) { + File[] fileList = getFileDirectory.listFiles(); + if (fileList != null && fileList.length > 0) { + for (File file : fileList) { + file.delete(); + } + } + } + } + +} diff --git a/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java b/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java new file mode 100644 index 000000000000..ec43ac2dd3a8 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java @@ -0,0 +1,21 @@ +package com.nmc.android.utils; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +public class KeyboardUtils { + + public static void showSoftKeyboard(Context context, View view) { + view.requestFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + + public static void hideKeyboardFrom(Context context, View view) { + view.clearFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/ScanBotSdkUtils.kt b/app/src/main/java/com/nmc/android/utils/ScanBotSdkUtils.kt new file mode 100644 index 000000000000..c1a301019bf5 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/ScanBotSdkUtils.kt @@ -0,0 +1,54 @@ +package com.nmc.android.utils + +import android.app.Activity +import android.graphics.Bitmap +import com.owncloud.android.lib.common.utils.Log_OC +import io.scanbot.sdk.ScanbotSDK +import kotlin.math.roundToInt + +object ScanBotSdkUtils { + private val TAG = ScanBotSdkUtils::class.java.simpleName + + //license key will be valid for application id: com.t_systems.android.webdav & com.t_systems.android.webdav.beta + //License validity till 25th March 2025 + const val LICENSE_KEY = "Z+GkfzcfWnNtCoGp4OsH9EJg4OuN5v" + + "BDhPFzHkhecpQaOS4s/r3qRPvKtgpG" + + "Q89KqfbvPC9Bwx/rPE7GYMmh+YnFIV" + + "wMD3HcGr4X0ETbH8JdsVP7njFJ5+yi" + + "xqlS3aSBh3GWtKT+umoTAzXbqF0ZS/" + + "EGXg0AhwWpQ7Fp+fyNMLwJTxt9/6Ya" + + "MZ2C0+MVwZyauKjeglILGZrcfenFR+" + + "a1LjBexcBigcqpMqsd6pDIBwtdp8RY" + + "spCuYgyQ6Vfb+DYbPts6ynFxXR1bsq" + + "TRcWBfkVMXIyCSNqgGStHCOZlVvqKo" + + "anolbemQEGz9lDtigeQN/4txtKX0L9" + + "2PLfqq6rOh/w==\nU2NhbmJvdFNESw" + + "pjb20udF9zeXN0ZW1zLmFuZHJvaWQu" + + "d2ViZGF2fGNvbS50X3N5c3RlbXMuYW" + + "5kcm9pZC53ZWJkYXYuYmV0YQoxNzQ1" + + "NjI1NTk5CjExNTU2NzgKMg==\n" + + @JvmStatic + fun isScanBotLicenseValid(activity: Activity): Boolean { + // Check the license status: + val licenseInfo = ScanbotSDK(activity).licenseInfo + Log_OC.d(TAG, "License status: ${licenseInfo.status}") + Log_OC.d(TAG, "License isValid: ${licenseInfo.isValid}") + + // Making your call into ScanbotSDK API is safe now. + // e.g. start barcode scanner + return licenseInfo.isValid + } + + @JvmStatic + fun resizeForPreview(bitmap: Bitmap): Bitmap { + val maxW = 1000f + val maxH = 1000f + val oldWidth = bitmap.width.toFloat() + val oldHeight = bitmap.height.toFloat() + val scaleFactor = if (oldWidth > oldHeight) maxW / oldWidth else maxH / oldHeight + val scaledWidth = (oldWidth * scaleFactor).roundToInt() + val scaledHeight = (oldHeight * scaleFactor).roundToInt() + return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 4afaf9fcbf6b..9263406d19c4 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -64,6 +64,7 @@ import com.nextcloud.utils.extensions.ContextExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.nmc.android.ui.LauncherActivity; +import com.nmc.android.utils.ScanBotSdkUtils; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -120,6 +121,9 @@ import dagger.android.DispatchingAndroidInjector; import dagger.android.HasAndroidInjector; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.scanbot.sap.SdkFeature; +import io.scanbot.sdk.ScanbotSDKInitializer; +import io.scanbot.sdk.core.contourdetector.ContourDetector; import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP; @@ -385,9 +389,9 @@ public void onCreate() { if (!MDMConfig.INSTANCE.sendFilesSupport(this)) { disableDocumentsStorageProvider(); } - - - } + + initialiseScanBotSDK(); + } public void disableDocumentsStorageProvider() { String packageName = getPackageName(); @@ -697,6 +701,10 @@ public static void notificationChannels() { createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string .notification_channel_general_name, R.string.notification_channel_general_description, context, NotificationManager.IMPORTANCE_DEFAULT); + + createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_IMAGE_SAVE, + R.string.notification_channel_image_save, + R.string.notification_channel_image_save_description, context); } else { Log_OC.e(TAG, "Notification manager is null"); } @@ -846,7 +854,7 @@ private static void updateToAutoUpload(Context context) { } } - + private static void showAutoUploadAlertDialog(Context context) { new MaterialAlertDialogBuilder(context, R.style.Theme_ownCloud_Dialog) @@ -1012,4 +1020,25 @@ public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailab backgroundJobManager.startOfflineOperations(); } } + + /** + * method to initialise the ScanBot SDK + */ + private void initialiseScanBotSDK() { + new ScanbotSDKInitializer() + .withLogging(BuildConfig.DEBUG, BuildConfig.DEBUG) + .license(this, ScanBotSdkUtils.LICENSE_KEY) + .contourDetectorType(ContourDetector.Type.ML_BASED) // ML_BASED is default. Set it to EDGE_BASED to use the edge-based approach + .licenceErrorHandler((status, sdkFeature, statusMessage) -> { + // Handle license errors here: + Log_OC.d(TAG, "License status: " + status.name()); + if (sdkFeature != SdkFeature.NoSdkFeature) { + Log_OC.d(TAG, "Missing SDK feature in license: " + sdkFeature.name()); + } + }) + //enable sdkFilesDir if custom file directory has to be set + //.sdkFilesDirectory(this,getExternalFilesDir(null)) + .prepareOCRLanguagesBlobs(true) + .initialize(this); + } } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderIfNotExistOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderIfNotExistOperation.java new file mode 100644 index 000000000000..25ecc07541d7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderIfNotExistOperation.java @@ -0,0 +1,72 @@ +/* + * ownCloud Android client application + * + * @author masensio + * @author Andy Scherzinger + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.operations; + +import android.content.Context; + +import com.nextcloud.client.account.User; +import com.owncloud.android.datamodel.FileDataStorageManager; +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation; +import com.owncloud.android.operations.common.SyncOperation; + +/** + * create folder only if it doesn't exist in remote + */ +public class CreateFolderIfNotExistOperation extends SyncOperation { + + private static final String TAG = CreateFolderIfNotExistOperation.class.getSimpleName(); + + private final String mRemotePath; + private final User user; + private final Context context; + private boolean isCheckOnlyFolderExistence; //flag to check if folder exist or not and will not create folder if flag is true + + public CreateFolderIfNotExistOperation(String remotePath, User user, boolean isCheckOnlyFolderExistence, Context context, FileDataStorageManager storageManager) { + super(storageManager); + this.mRemotePath = remotePath; + this.user = user; + this.context = context; + this.isCheckOnlyFolderExistence = isCheckOnlyFolderExistence; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + RemoteOperation operation = new ExistenceCheckRemoteOperation(mRemotePath, false); + RemoteOperationResult result = operation.execute(client); + + if (isCheckOnlyFolderExistence) { + return result; + } + + //if remote folder doesn't exist then create it else ignore it + if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND) { + SyncOperation syncOp = new CreateFolderOperation(mRemotePath, user, context, getStorageManager()); + result = syncOp.execute(client); + } + + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index 0cd2d5e30a31..817f0d5c0009 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -49,6 +49,7 @@ import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation; import com.owncloud.android.operations.CheckCurrentCredentialsOperation; import com.owncloud.android.operations.CopyFileOperation; +import com.owncloud.android.operations.CreateFolderIfNotExistOperation; import com.owncloud.android.operations.CreateFolderOperation; import com.owncloud.android.operations.CreateShareViaLinkOperation; import com.owncloud.android.operations.CreateShareWithShareeOperation; @@ -100,6 +101,7 @@ public class OperationsService extends Service { public static final String EXTRA_SHARE_ID = "SHARE_ID"; public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE"; public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND"; + public static final String EXTRA_CHECK_ONLY_FOLDER_EXISTENCE = "CHECK_ONLY_FOLDER_EXISTENCE"; public static final String ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK"; public static final String ACTION_CREATE_SECURE_FILE_DROP = "CREATE_SECURE_FILE_DROP"; @@ -120,6 +122,7 @@ public class OperationsService extends Service { public static final String ACTION_COPY_FILE = "COPY_FILE"; public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS"; public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION"; + public static final String ACTION_CREATE_FOLDER_NOT_EXIST = "CREATE_FOLDER_NOT_EXIST"; private ServiceHandler mOperationsHandler; private OperationsServiceBinder mOperationsBinder; @@ -691,6 +694,13 @@ private Pair newOperation(Intent operationIntent) { fileDataStorageManager); break; + case ACTION_CREATE_FOLDER_NOT_EXIST: + remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + operation = new CreateFolderIfNotExistOperation(remotePath, user, + operationIntent.getBooleanExtra(EXTRA_CHECK_ONLY_FOLDER_EXISTENCE, false), + getApplicationContext(), fileDataStorageManager); + break; + case ACTION_SYNC_FILE: remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 1fa32f5e3309..05e2af0d03d6 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -80,6 +80,7 @@ import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.SyncedFolder; import com.owncloud.android.datamodel.SyncedFolderProvider; +import com.nmc.android.scans.SaveScannedDocumentFragment; import com.owncloud.android.datamodel.VirtualFolderType; import com.owncloud.android.files.services.NameCollisionPolicy; import com.owncloud.android.lib.common.OwnCloudClient; @@ -209,6 +210,7 @@ public class FileDisplayActivity extends FileActivity public static final int REQUEST_CODE__MOVE_OR_COPY_FILES = REQUEST_CODE__LAST_SHARED + 3; public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5; public static final int REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA = REQUEST_CODE__LAST_SHARED + 6; + public static final int REQUEST_CODE__SCAN_DOCUMENT = REQUEST_CODE__LAST_SHARED + 9; protected static final long DELAY_TO_REQUEST_REFRESH_OPERATION_LATER = DELAY_TO_REQUEST_OPERATIONS_LATER + 350; @@ -301,19 +303,19 @@ private void checkAutoUploadOnGPlay() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return; } - + if (PermissionUtil.checkSelfPermission(this, Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { return; } - + if (preferences.isAutoUploadGPlayWarningShown()) { return; } - + boolean showInfoDialog = false; for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) { // move or delete after success - if (syncedFolder.getUploadAction() == FileUploadWorker.LOCAL_BEHAVIOUR_MOVE || + if (syncedFolder.getUploadAction() == FileUploadWorker.LOCAL_BEHAVIOUR_MOVE || syncedFolder.getUploadAction() == FileUploadWorker.LOCAL_BEHAVIOUR_DELETE) { showInfoDialog = true; break; @@ -959,6 +961,26 @@ public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String. exitSelectionMode(); } else if (requestCode == PermissionUtil.REQUEST_CODE_MANAGE_ALL_FILES) { syncAndUpdateFolder(true); + } else if (requestCode == REQUEST_CODE__SCAN_DOCUMENT && resultCode == RESULT_OK) { + + OCFile remoteFilePath = data.getParcelableExtra(SaveScannedDocumentFragment.EXTRA_SCAN_DOC_REMOTE_PATH); + if (remoteFilePath == null) { + remoteFilePath = getCurrentDir(); + } + + Log_OC.d(this, "Scan Document save remote path: " + remoteFilePath.getRemotePath()); + + // NMC-2418 fix + if (remoteFilePath.getRemotePath().equals(getCurrentDir().getRemotePath())) { + Log_OC.d(this, "Both current and scan paths are same. Skipping redirection."); + return; + } + + OCFileListFragment fileListFragment = getListOfFilesFragment(); + if (fileListFragment != null) { + fileListFragment.onItemClicked(remoteFilePath); + } + } else { super.onActivityResult(requestCode, resultCode, data); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 30dc48141c3e..80b592f08e1b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -61,6 +61,8 @@ open class FolderPickerActivity : private var mSyncBroadcastReceiver: SyncBroadcastReceiver? = null private var mSyncInProgress = false private var mSearchOnlyFolders = false + private var mShowOnlyFolder = false + private var mHideEncryptedFolder = false var isDoNotEnterEncryptedFolder = false private set @@ -114,12 +116,27 @@ open class FolderPickerActivity : } private fun setupAction() { + mShowOnlyFolder = intent.getBooleanExtra(EXTRA_SHOW_ONLY_FOLDER, false) + mHideEncryptedFolder = intent.getBooleanExtra(EXTRA_HIDE_ENCRYPTED_FOLDER, false) + action = intent.getStringExtra(EXTRA_ACTION) - if (action != null && action == CHOOSE_LOCATION) { - setupUIForChooseButton() + if (action != null) { + when (action) { + MOVE_OR_COPY -> { + captionText = resources.getText(R.string.folder_picker_choose_caption_text).toString() + mSearchOnlyFolders = true + isDoNotEnterEncryptedFolder = true + } + + CHOOSE_LOCATION -> { + setupUIForChooseButton() + } + + else -> configureDefaultCase() + } } else { - captionText = themeUtils.getDefaultDisplayNameForRootFolder(this) + configureDefaultCase() } } @@ -128,9 +145,12 @@ open class FolderPickerActivity : } private fun setupUIForChooseButton() { - captionText = resources.getText(R.string.folder_picker_choose_caption_text).toString() + captionText = resources.getText(R.string.choose_location).toString() mSearchOnlyFolders = true - isDoNotEnterEncryptedFolder = true + // NMC-3671 fix + // allow entering into e2ee folder while choosing location + isDoNotEnterEncryptedFolder = false + mShowOnlyFolder = true if (this is FilePickerActivity) { return @@ -138,11 +158,18 @@ open class FolderPickerActivity : folderPickerBinding.folderPickerBtnCopy.visibility = View.GONE folderPickerBinding.folderPickerBtnMove.visibility = View.GONE folderPickerBinding.folderPickerBtnChoose.visibility = View.VISIBLE - folderPickerBinding.chooseButtonSpacer.visibility = View.VISIBLE folderPickerBinding.moveOrCopyButtonSpacer.visibility = View.GONE + + // NMC Customization + folderPickerBinding.folderPickerBtnChoose.text = resources.getString(R.string.common_select) } } + // NMC Customization + private fun configureDefaultCase() { + captionText = themeUtils.getDefaultDisplayNameForRootFolder(this) + } + private fun handleOnBackPressed() { onBackPressedDispatcher.addCallback( this, @@ -359,7 +386,7 @@ open class FolderPickerActivity : } private fun refreshListOfFilesFragment(fromSearch: Boolean) { - listOfFilesFragment?.listDirectory(false, fromSearch) + listOfFilesFragment?.listDirectoryFolder(false, fromSearch, mShowOnlyFolder, mHideEncryptedFolder) } fun browseToRoot() { @@ -686,6 +713,12 @@ open class FolderPickerActivity : @JvmField val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION") + @JvmField + val EXTRA_SHOW_ONLY_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_SHOW_ONLY_FOLDER") + + @JvmField + val EXTRA_HIDE_ENCRYPTED_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_HIDE_ENCRYPTED_FOLDER") + const val MOVE_OR_COPY = "MOVE_OR_COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" private val TAG = FolderPickerActivity::class.java.simpleName diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index 0e56749caa79..3d74d16ff1d8 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java @@ -64,6 +64,7 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable private LinearLayout mInfoBox; private TextView mInfoBoxMessage; protected AppCompatSpinner mToolbarSpinner; + private View mDefaultToolbarDivider; private boolean isHomeSearchToolbarShow = false; @Inject public ThemeColorUtils themeColorUtils; @@ -85,6 +86,7 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mSearchText = findViewById(R.id.search_text); mSwitchAccountButton = findViewById(R.id.switch_account_button); mNotificationButton = findViewById(R.id.notification_button); + mDefaultToolbarDivider = findViewById(R.id.default_toolbar_divider); if (showSortListButtonGroup) { findViewById(R.id.sort_list_button_group).setVisibility(View.VISIBLE); @@ -187,6 +189,14 @@ private void showHomeSearchToolbar(boolean isShow) { } } + public void showHideToolbar(boolean isShow){ + mDefaultToolbar.setVisibility(isShow ? View.VISIBLE : View.GONE); + } + + public void showHideDefaultToolbarDivider(boolean isShow) { + mDefaultToolbarDivider.setVisibility(isShow ? View.VISIBLE : View.GONE); + } + /** * Updates title bar and home buttons (state and icon). */ diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 4e9bc9bd1dec..60ed6b98bc62 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -74,6 +74,7 @@ import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; +import com.owncloud.android.utils.MimeType; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -832,6 +833,65 @@ public void swapDirectory( notifyDataSetChanged(); } + /** + * method will only called if only folders has to show + */ + public void showOnlyFolder( + User account, + OCFile directory, + FileDataStorageManager updatedStorageManager, + boolean onlyOnDevice, String limitToMimeType, + boolean hideEncryptedFolder + ) { + this.onlyOnDevice = onlyOnDevice; + + if (updatedStorageManager != null && !updatedStorageManager.equals(mStorageManager)) { + mStorageManager = updatedStorageManager; + ocFileListDelegate.setShowShareAvatar(mStorageManager + .getCapability(user.getAccountName()) + .getVersion() + .isShareesOnDavSupported()); + this.user = account; + } + + if (mStorageManager != null) { + + List allFiles = mStorageManager.getFolderContent(directory, onlyOnDevice); + mFiles.clear(); + + for (int i = 0; i < allFiles.size(); i++) { + OCFile ocFile = allFiles.get(i); + + //if e2ee folder has to hide then ignore if OCFile is encrypted + if (hideEncryptedFolder && ocFile.isEncrypted()) { + continue; + } + + if (ocFile.getMimeType().equals(MimeType.DIRECTORY)) { + mFiles.add(allFiles.get(i)); + } + } + + if (!preferences.isShowHiddenFilesEnabled()) { + mFiles = filterHiddenFiles(mFiles); + } + if (!limitToMimeType.isEmpty()) { + mFiles = filterByMimeType(mFiles, limitToMimeType); + } + FileSortOrder sortOrder = preferences.getSortOrderByFolder(directory); + mFiles = sortOrder.sortCloudFiles(mFiles); + mFilesAll.clear(); + mFilesAll.addAll(mFiles); + + currentDirectory = directory; + } else { + mFiles.clear(); + mFilesAll.clear(); + } + + new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged); + } + /** * Converts Offline Operations to OCFiles and adds them to the adapter for visual feedback. * This function creates pending OCFiles, but they may not consistently appear in the UI. diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java index c28f1e9837f9..e134e7c5f757 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java @@ -28,6 +28,11 @@ public interface OCFileListBottomSheetActions { */ void uploadFiles(); + /** + * offers a file scanner + */ + void scanDocument(); + /** * opens template selection for documents */ diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java index f5458da9e546..a4e8c0c37ad4 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java @@ -30,6 +30,7 @@ import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.ThemeUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; +import com.nmc.android.utils.ScanBotSdkUtils; /** * FAB menu {@link android.app.Dialog} styled as a bottom sheet for main actions. @@ -134,6 +135,13 @@ protected void onCreate(Bundle savedInstanceState) { if (!deviceInfo.hasCamera(getContext())) { binding.menuDirectCameraUpload.setVisibility(View.GONE); + binding.menuScanDocument.setVisibility(View.GONE); + } + + //check if scanbot sdk licence is valid or not + //hide the view if license is not valid + if(!ScanBotSdkUtils.isScanBotLicenseValid(fileActivity)){ + // binding.menuScanDocument.setVisibility(View.GONE); } // create rich workspace @@ -190,6 +198,11 @@ private void setupClickListener() { binding.menuScanDocUpload.setVisibility(View.GONE); } + binding.menuScanDocument.setOnClickListener(v -> { + actions.scanDocument(); + dismiss(); + }); + binding.menuUploadFiles.setOnClickListener(v -> { actions.uploadFiles(); 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 7e4be1179a03..34f9ecf2711d 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 @@ -61,6 +61,7 @@ import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; import com.nextcloud.utils.fileNameValidator.FileNameValidator; +import com.nmc.android.marketTracking.TrackingScanInterface; import com.nextcloud.utils.view.FastScrollUtils; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -81,6 +82,7 @@ import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.ui.activity.DrawerActivity; +import com.nmc.android.scans.ScanActivity; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; @@ -239,6 +241,13 @@ public class OCFileListFragment extends ExtendedListFragment implements @Inject DeviceInfo deviceInfo; + /** + * Things to note about both the branches. 1. nmc/1867-scanbot branch: --> interface won't be initialised --> + * calling of interface method will be done here 2. nmc/1925-market_tracking --> interface will be initialised --> + * calling of interface method won't be done here + */ + private TrackingScanInterface trackingScanInterface; + protected enum MenuItemAddRemove { DO_NOTHING, REMOVE_SORT, @@ -246,6 +255,8 @@ protected enum MenuItemAddRemove { ADD_GRID_AND_SORT_WITH_SEARCH } + private boolean mShowOnlyFolder, mHideEncryptedFolder; + protected MenuItemAddRemove menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH; private List mOriginalMenuItems = new ArrayList<>(); @@ -603,6 +614,24 @@ public void createRichWorkspace() { }).start(); } + @Override + public void scanDocument() { + //remote file to store the scans in the selected path + OCFile remoteFile = new OCFile(ROOT_PATH); // default root folder + if (getActivity() != null && ((FileActivity) getActivity()).getCurrentDir() != null){ + remoteFile = ((FileActivity) getActivity()).getCurrentDir(); + } + + //remote path used so that user can directly save at the selected sub folder location + ScanActivity.openScanActivity(getActivity(), remoteFile, FileDisplayActivity.REQUEST_CODE__SCAN_DOCUMENT); + + //track event on Scan Document button click + //implementation and logic will be available in nmc/1925-market_tracking + if (trackingScanInterface != null) { + trackingScanInterface.sendScanEvent(preferences); + } + } + @Override public void onShareIconClick(OCFile file) { if (file.isFolder()) { @@ -1468,6 +1497,12 @@ public void listDirectory(boolean onlyOnDevice, boolean fromSearch) { listDirectory(null, onlyOnDevice, fromSearch); } + public void listDirectoryFolder(boolean onlyOnDevice, boolean fromSearch, boolean showOnlyFolder, boolean hideEncryptedFolder) { + mShowOnlyFolder = showOnlyFolder; + mHideEncryptedFolder = hideEncryptedFolder; + listDirectory(null, onlyOnDevice, fromSearch); + } + public void refreshDirectory() { searchFragment = false; @@ -1527,12 +1562,23 @@ public void listDirectory(OCFile directory, OCFile file, boolean onlyOnDevice, b return; } - mAdapter.swapDirectory( - accountManager.getUser(), - directory, - storageManager, - onlyOnDevice, - mLimitToMimeType); + if(mShowOnlyFolder) { + mAdapter.showOnlyFolder( + accountManager.getUser(), + directory, + storageManager, + onlyOnDevice, + mLimitToMimeType, + mHideEncryptedFolder); + } + else { + mAdapter.swapDirectory( + accountManager.getUser(), + directory, + storageManager, + onlyOnDevice, + mLimitToMimeType); + } OCFile previousDirectory = mFile; mFile = directory; diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 6b765c2afd2d..dfa00dbec8db 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -960,6 +960,23 @@ public void createFolder(String remotePath) { fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); } + /** + * create folder in remote if it doesn't exist if it exist then we will ignore creating + * + * @param remotePath to be created + * @param isCheckOnlyFolderExistence to check only folder exists or not and will not create folder if flag is true + */ + public void createFolderIfNotExist(String remotePath, boolean isCheckOnlyFolderExistence) { + Intent service = new Intent(fileActivity, OperationsService.class); + service.setAction(OperationsService.ACTION_CREATE_FOLDER_NOT_EXIST); + service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); + service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath); + service.putExtra(OperationsService.EXTRA_CHECK_ONLY_FOLDER_EXISTENCE, isCheckOnlyFolderExistence); + mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); + + fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); + } + /** * Cancel the transference in downloads (files/folders) and file uploads * diff --git a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java index 5a529dd90d4e..1119bf54a9e2 100644 --- a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java +++ b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java @@ -34,6 +34,7 @@ public final class NotificationUtils { public static final String NOTIFICATION_CHANNEL_FILE_OBSERVER = "NOTIFICATION_CHANNEL_FILE_OBSERVER"; public static final String NOTIFICATION_CHANNEL_PUSH = "NOTIFICATION_CHANNEL_PUSH"; public static final String NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS = "NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS"; + public static final String NOTIFICATION_CHANNEL_IMAGE_SAVE = "NOTIFICATION_CHANNEL_IMAGE_SAVE"; private NotificationUtils() { // utility class -> private constructor diff --git a/app/src/main/java/com/owncloud/android/utils/StringUtils.java b/app/src/main/java/com/owncloud/android/utils/StringUtils.java index d4339f0003eb..5eb8f2558007 100644 --- a/app/src/main/java/com/owncloud/android/utils/StringUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/StringUtils.java @@ -15,6 +15,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Arrays; +import java.util.List; + /** * Helper class for handling and manipulating strings. */ @@ -24,6 +27,10 @@ private StringUtils() { // prevent class from being constructed } + public static List convertStringToList(String input) { + return Arrays.asList(input.split("\\s*,\\s*")); + } + public static @NonNull String searchAndColor(@Nullable String text, @Nullable String searchText, @ColorInt int color) { diff --git a/app/src/main/res/drawable-xxhdpi/ui_crop_magnifier.png b/app/src/main/res/drawable-xxhdpi/ui_crop_magnifier.png new file mode 100644 index 000000000000..052b0b14c3cd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ui_crop_magnifier.png differ diff --git a/app/src/main/res/drawable/grey_curve_bg.xml b/app/src/main/res/drawable/grey_curve_bg.xml new file mode 100644 index 000000000000..9e46eb482628 --- /dev/null +++ b/app/src/main/res/drawable/grey_curve_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/grey_transparent_curve_bg.xml b/app/src/main/res/drawable/grey_transparent_curve_bg.xml new file mode 100644 index 000000000000..39ef7905d095 --- /dev/null +++ b/app/src/main/res/drawable/grey_transparent_curve_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_format_shapes_24.xml b/app/src/main/res/drawable/ic_baseline_format_shapes_24.xml new file mode 100644 index 000000000000..eb9dbe44571f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_format_shapes_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_crop.xml b/app/src/main/res/drawable/ic_crop.xml new file mode 100644 index 000000000000..8c81a5219347 --- /dev/null +++ b/app/src/main/res/drawable/ic_crop.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 000000000000..072ee2938d3b --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_flash.xml b/app/src/main/res/drawable/ic_flash.xml new file mode 100644 index 000000000000..f03889c61bbc --- /dev/null +++ b/app/src/main/res/drawable/ic_flash.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_magentacloud.xml b/app/src/main/res/drawable/ic_magentacloud.xml new file mode 100644 index 000000000000..e1931f595463 --- /dev/null +++ b/app/src/main/res/drawable/ic_magentacloud.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_pencil_edit.xml b/app/src/main/res/drawable/ic_pencil_edit.xml new file mode 100644 index 000000000000..a1089345a7b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil_edit.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_remove.xml b/app/src/main/res/drawable/ic_remove.xml new file mode 100644 index 000000000000..f0dd8d6fa79c --- /dev/null +++ b/app/src/main/res/drawable/ic_remove.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_rotate_right.xml b/app/src/main/res/drawable/ic_rotate_right.xml new file mode 100644 index 000000000000..4c96b1f30453 --- /dev/null +++ b/app/src/main/res/drawable/ic_rotate_right.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_scan_add.xml b/app/src/main/res/drawable/ic_scan_add.xml new file mode 100644 index 000000000000..984f05d795fe --- /dev/null +++ b/app/src/main/res/drawable/ic_scan_add.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ui_crop_corner_handle.xml b/app/src/main/res/drawable/ui_crop_corner_handle.xml new file mode 100644 index 000000000000..66d6003a6f06 --- /dev/null +++ b/app/src/main/res/drawable/ui_crop_corner_handle.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_crop_side_handle.xml b/app/src/main/res/drawable/ui_crop_side_handle.xml new file mode 100644 index 000000000000..74ad23e29654 --- /dev/null +++ b/app/src/main/res/drawable/ui_crop_side_handle.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_scan.xml b/app/src/main/res/layout/activity_scan.xml new file mode 100644 index 000000000000..a20eecae34d2 --- /dev/null +++ b/app/src/main/res/layout/activity_scan.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml b/app/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml index 947a24fe27f6..f6b684af55b9 100644 --- a/app/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml +++ b/app/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml @@ -136,6 +136,38 @@ + + + + + + + + - - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_scanned_document.xml b/app/src/main/res/layout/fragment_edit_scanned_document.xml new file mode 100644 index 000000000000..44363cf2d671 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_scanned_document.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_scan_document.xml b/app/src/main/res/layout/fragment_scan_document.xml new file mode 100644 index 000000000000..a45c05ec53b3 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_document.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_scan_save.xml b/app/src/main/res/layout/fragment_scan_save.xml new file mode 100644 index 000000000000..4b5c01b06ae2 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_save.xml @@ -0,0 +1,429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_scanned_doc.xml b/app/src/main/res/layout/item_scanned_doc.xml new file mode 100644 index 000000000000..1d79e69e600d --- /dev/null +++ b/app/src/main/res/layout/item_scanned_doc.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_standard.xml b/app/src/main/res/layout/toolbar_standard.xml index 376909c641f6..b6a0d303ce2d 100644 --- a/app/src/main/res/layout/toolbar_standard.xml +++ b/app/src/main/res/layout/toolbar_standard.xml @@ -112,6 +112,14 @@ app:popupTheme="@style/Theme.AppCompat.DayNight.NoActionBar" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/nmc_scan_strings.xml b/app/src/main/res/values-de/nmc_scan_strings.xml new file mode 100644 index 000000000000..22461c5c3c5b --- /dev/null +++ b/app/src/main/res/values-de/nmc_scan_strings.xml @@ -0,0 +1,57 @@ + + + + Dokument scannen + Nicht bewegen + Näher heranbewegen + Perspektive + Kein Dokument + Hintergrund zu unruhig + Falsches Bildformat.\nDrehen Sie Ihr Gerät. + Schwaches Licht + %d von %d + Scan bearbeiten + Scan beschneiden + Rahmen zurücksetzen + Dokument erkennen + Filter anwenden + Kein Filter + Whiteboard + Foto Filter + Schwarz-Weiß + Dokument Filter + Grau + Automatisch + Blitz + Speichern unter + Dateiname + Speicherort + /Hauptverzeichnis + Dateityp + Speichern ohne Texterkennung + Speichern mit Texterkennung + Textdokument (txt) + PDF-Passwort + Passwort setzen + Bitten wählen sie mindestens einen Dateityp zum Speichern aus. + Bitte geben Sie ein Passwort für das zu erstellende PDF ein oder deaktivieren Sie die Funktion. + Sie können die gescannten Dokumente mit oder ohne Texterkennung abspeichern. Sie können auch mehrere Dateiformate auswählen. + Sie können keine Dokumente scannen ohne die Erlaubnis die Kamera zu verwenden. + Weiteres Dokument hinzufügen + Gescanntes Dokument zuschneiden + Gescanntes Dokument filtern + Gescanntes Dokument drehen + Gescanntes Dokument löschen + Scan-Dateinamen bearbeiten + Scan-Speicherort bearbeiten + Ok + Das Speichern kann einige Minuten in Anspruch nehmen, insbesondere wenn Sie mehrere Seiten und Dateiformate ausgewählt haben. + Dateien werden gespeichert… + Benachrichtigungskanal zum Speichern von Bildern + Zeigt den Fortschritt der Bildspeicherung an + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 70c308e53467..fec8f19fb02f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -133,6 +133,7 @@ Schauen Sie später noch einmal vorbei oder laden Sie neu. Checkbox Wähle einen lokalen Ordner… + Ort wählen Wähle einen entfernten Ordner … Bitte eine Vorlage auswählen und einen Dateinamen eingeben. Wählen Sie, welche Datei behalten werden soll! @@ -163,6 +164,7 @@ Löschen Umbenennen Speichern + Auswählen Senden Teilen Überspringen @@ -1076,6 +1078,7 @@ Herunterladen Video Überlagerungsicon Bitte warten… + Bitte geben Sie unter Apps & Benachrichtigungen in den Einstellungen manuell die Erlaubnis. Überprüfe gespeicherte Anmeldeinformationen Kopiere Datei von privatem Speicher Das Ändern der Erweiterung kann dazu führen, dass diese Datei in einer anderen Anwendung geöffnet wird diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 8972d2bb6716..6488018f11a5 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -38,4 +38,68 @@ @android:color/white #101418 + + + #FFFFFF + @color/grey_30 + @color/grey_30 + #CCCCCC + @color/grey_70 + @color/grey_80 + #2D2D2D + @color/grey_70 + @color/grey_70 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_60 + @color/grey_0 + @color/grey_0 + @color/grey_30 + #FFFFFF + @color/grey_30 + @color/grey_80 + #FFFFFF + + + @color/grey_80 + @color/grey_30 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + @color/grey_80 + + + @color/grey_70 + @color/grey_60 + + + @color/grey_70 + @color/grey_70 + + + #FFFFFF + @color/grey_30 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_60 + @color/grey_0 + #FFFFFF + + + #121212 + @color/grey_0 + @color/grey_80 + @color/grey_80 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e0da603d4f8a..66637ab603ae 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -78,4 +78,93 @@ #A5A5A5 #F7F9FF + + + #191919 + @color/primary + #191919 + #191919 + @color/grey_30 + @android:color/white + #FFFFFF + @color/grey_0 + #CCCCCC + #77c4ff + #B3FFFFFF + @color/grey_10 + + + #101010 + #F2F2F2 + #E5E5E5 + #B2B2B2 + #666666 + #4C4C4C + #333333 + + + @color/design_snackbar_background_color + @color/white + + + #FFFFFF + #191919 + + + @color/grey_0 + #191919 + @color/primary + #191919 + @color/primary + @color/grey_30 + @color/white + #191919 + + + #FFFFFF + #191919 + #191919 + + + #FFFFFF + #191919 + #FFFFFF + + + @color/primary + #F399C7 + #FFFFFF + @color/grey_30 + @color/grey_10 + @color/grey_0 + + + @color/primary + @color/grey_30 + @color/grey_30 + #CCCCCC + + + #191919 + @color/grey_30 + #191919 + #191919 + #191919 + #191919 + @color/grey_30 + #191919 + #000000 + #191919 + #F6E5EB + #C16F81 + #0D39DF + #0099ff + + + @color/grey_0 + #191919 + @color/grey_0 + @color/grey_30 + #77b6bb + #5077b6bb diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..cc9e25255a10 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,31 @@ + + + 4dp + 16dp + 24dp + 6dp + 18sp + 15sp + 15dp + 56dp + 86dp + 80dp + 11sp + 30dp + 55dp + 258dp + 17sp + 20dp + 160dp + 50dp + 150dp + 55dp + 48dp + 48dp + 24dp + 26dp + 20sp + 145dp + 1dp + 13sp + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_scan_attrs.xml b/app/src/main/res/values/nmc_scan_attrs.xml new file mode 100644 index 000000000000..d69dace876c4 --- /dev/null +++ b/app/src/main/res/values/nmc_scan_attrs.xml @@ -0,0 +1,11 @@ + + + + @string/edit_scan_filter_none + @string/edit_scan_filter_color_enhanced + @string/edit_scan_filter_color_document + @string/edit_scan_filter_grey + @string/edit_scan_filter_b_n_w + @string/edit_scan_filter_pure_binarized + + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_scan_strings.xml b/app/src/main/res/values/nmc_scan_strings.xml new file mode 100644 index 000000000000..cffc108323e3 --- /dev/null +++ b/app/src/main/res/values/nmc_scan_strings.xml @@ -0,0 +1,61 @@ + + + + Scan Document + Do not move + Move closer + Perspective + No document + Background too noisy + Wrong aspect ratio.\nRotate your device. + Poor light + %d of %d + Edit Scan + Crop Scan + Reset Crop + Detect Document + Apply Filter + No Filter + Whiteboard + Photo Filter + Black & White + Document Filter + Grayscale + Automatic + Flash + Save as + Filename + Location + /Root folder + File type + Save without text recognition + Save with text recognition + Textfile (txt) + PDF-Password + Set password + Please select at least one filetype + Please enter a password for the PDF you want to create or disable the function. + You can save the file with or without text recognition. Multiple selection is allowed. + You cannot scan document without camera permission. + Add more document + Crop scanned document + Filter scanned document + Rotate scanned document + Delete scanned document + Edit scan filename + Edit scan location + Ok + Saving will take some time, especially if you have selected several pages and file formats. + PDF + JPG + PNG + PDF (OCR) + Saving files… + Image save notification channel + Shows image save progress + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8081780ac3f0..6d70de5a5c6c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1150,6 +1150,7 @@ Link Name Delete Link Settings + Please navigate to App info in settings and give permission manually. Confirm Strict mode: no HTTP connection allowed! Destination filename @@ -1181,6 +1182,8 @@ Error creating file from template No app available for sending the selected files Tap on a page to zoom in + Choose location + Select Full access Media read-only Photos and videos diff --git a/nmc_scan-dependencies.gradle b/nmc_scan-dependencies.gradle new file mode 100644 index 000000000000..ede22d919bc7 --- /dev/null +++ b/nmc_scan-dependencies.gradle @@ -0,0 +1,19 @@ +repositories { + // Scanbot SDK maven repos: + maven { + url 'https://nexus.scanbot.io/nexus/content/repositories/releases/' + } + maven { + url 'https://nexus.scanbot.io/nexus/content/repositories/snapshots/' + } +} + +dependencies { + // scanbot sdk: https://github.com/doo/scanbot-sdk-example-android + implementation "io.scanbot:sdk-package-2:4.0.0" + + // apache pdf-box for encrypting pdf files + implementation(group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.1') { + exclude group: "commons-logging" + } +} \ No newline at end of file