diff --git a/app/build.gradle b/app/build.gradle index e4caae5cebbf..b4d7ba5190c4 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 @@ -198,6 +200,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 } } @@ -315,10 +318,11 @@ dependencies { implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:$fidoVersion" implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:$fidoVersion" + //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.4' 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 78c6f415cf45..424e808e4e43 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 @@ -386,6 +386,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 77bfd0b42305..c39848fc0b0f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -109,6 +109,7 @@ + + + + \ No newline at end of file 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 c149e25c0869..44ab00b34330 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -30,6 +30,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; @@ -444,6 +446,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 e10207a1f49b..a8c50dcabefa 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -27,6 +27,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 @@ -93,6 +94,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) else -> null // caller falls back to default factory @@ -266,6 +268,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 ae6d35a5c39a..6bb9f0645142 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -159,6 +159,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() @@ -168,4 +175,6 @@ interface BackgroundJobManager { fun schedulePeriodicHealthStatus() fun startHealthStatus() fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean + + 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 716440c36b7e..8ae73cdcdb78 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 @@ -32,8 +33,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 @@ -80,6 +83,7 @@ internal class BackgroundJobManagerImpl( const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" + const val JOB_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" @@ -604,6 +608,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) @@ -649,4 +673,21 @@ internal class BackgroundJobManagerImpl( 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 b2517b5fa579..b80a3018b6ea 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 3bfb6afc391b..b692882c07b1 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; @@ -79,6 +80,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"; @@ -694,6 +696,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..269ba13df94d --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/CropScannedDocumentFragment.kt @@ -0,0 +1,343 @@ +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 { + originalBitmap = onDocScanListener.getScannedDocs()[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..7ab63c37c016 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/SaveScannedDocumentFragment.kt @@ -0,0 +1,356 @@ +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 remotePath = (requireActivity() as ScanActivity).remotePath + //remote path should not be null and should not be root path i.e only / + if (remotePath != null && remotePath != OCFile.ROOT_PATH) { + setRemoteFilePath(remotePath) + 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(remotePath) + return + } + } + + setRemoteFilePath(appPreferences.uploadScansLastPath) + } + + fun setRemoteFilePath(remotePath: String) { + remoteFilePath = OCFile(remotePath) + remoteFilePath?.setFolder() + + updateSaveLocationText(remotePath) + } + + 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 + } + + 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(path: String) { + var newPath = path + remotePath = newPath + if (newPath.equals(OCFile.ROOT_PATH, ignoreCase = true)) { + newPath = resources.getString(R.string.scan_save_location_root) + } + binding.scanSaveLocationInput.text = newPath + } + + 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(chosenFolder.remotePath) + } + } + } + } + + 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..b6bf18bab913 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanActivity.kt @@ -0,0 +1,238 @@ +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.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.operations.CreateFolderIfNotExistOperation +import com.owncloud.android.ui.activity.FileActivity +import io.scanbot.sdk.ScanbotSDK + +class ScanActivity : FileActivity(), OnFragmentChangeListener, OnDocScanListener { + private lateinit var binding: ActivityScanBinding + lateinit var scanbotSDK: ScanbotSDK + + var remotePath: String? = 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) + remotePath = intent.getStringExtra(EXTRA_REMOTE_PATH) + 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 { + super.onBackPressed() + } + } + + 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() { + //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) + } + } + + 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, remotePath: String, requestCode: Int) { + val intent = Intent(context, ScanActivity::class.java) + intent.putExtra(EXTRA_REMOTE_PATH, remotePath) + (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..a40dfbce85af --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt @@ -0,0 +1,451 @@ +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 + || ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.READ_EXTERNAL_STORAGE + ) != PackageManager + .PERMISSION_GRANTED || ContextCompat.checkSelfPermission( + requireActivity(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != + PackageManager.PERMISSION_GRANTED + ) { + requestMultiplePermissions.launch( + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + ) + } + } + + 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 || Manifest.permission.READ_EXTERNAL_STORAGE == it.key + || Manifest.permission.WRITE_EXTERNAL_STORAGE == 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 2f2b17e9d044..42ea764dfc68 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -61,6 +61,7 @@ import com.nextcloud.utils.extensions.ContextExtensionsKt; import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.authentication.AuthenticatorActivity; +import com.nmc.android.utils.ScanBotSdkUtils; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -121,6 +122,9 @@ import de.cotech.hw.SecurityKeyManager; import de.cotech.hw.SecurityKeyManagerConfig; 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; @@ -373,6 +377,8 @@ public void onCreate() { backgroundJobManager.schedulePeriodicHealthStatus(); registerGlobalPassCodeProtection(); + + initialiseScanBotSDK(); } private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> { @@ -667,6 +673,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"); } @@ -961,4 +971,25 @@ public static void setAppTheme(DarkMode mode) { case SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); } } + + /** + * 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 4cf151b3f77e..968ec83b022b 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 5a36009fb8d8..267acad2ec20 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 @@ -73,6 +73,7 @@ import com.owncloud.android.databinding.FilesBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +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; @@ -199,6 +200,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; @@ -886,6 +888,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 f8fbc5c1efe4..64cde781f814 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 @@ -60,6 +60,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 @@ -113,12 +115,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() } } @@ -127,9 +144,10 @@ 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 + mShowOnlyFolder = true if (this is FilePickerActivity) { return @@ -137,11 +155,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, @@ -358,7 +383,7 @@ open class FolderPickerActivity : } private fun refreshListOfFilesFragment(fromSearch: Boolean) { - listOfFilesFragment?.listDirectory(false, fromSearch) + listOfFilesFragment?.listDirectoryFolder(false, fromSearch, mShowOnlyFolder, mHideEncryptedFolder) } fun browseToRoot() { @@ -660,6 +685,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/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt index 471390356e49..83f62dce73ce 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -64,6 +64,7 @@ class NotificationsActivity : DrawerActivity(), NotificationsContract.View { } setupToolbar() + showHideDefaultToolbarDivider(true) updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications)) setupDrawer(R.id.nav_notifications) 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 14f8ba64a26c..a58ee38b5eed 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 @@ -62,6 +62,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; @@ -82,6 +83,7 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mMenuButton = findViewById(R.id.menu_button); mSearchText = findViewById(R.id.search_text); mSwitchAccountButton = findViewById(R.id.switch_account_button); + mDefaultToolbarDivider = findViewById(R.id.default_toolbar_divider); if (showSortListButtonGroup) { findViewById(R.id.sort_list_button_group).setVisibility(View.VISIBLE); @@ -180,6 +182,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 3490e87b3f43..4b27c4d5040d 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 @@ -65,6 +65,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; @@ -757,6 +758,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); + } + public void setData(List objects, SearchType searchType, FileDataStorageManager storageManager, 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 2deb69e0537d..99f89fe82a80 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 @@ -189,6 +197,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 1d74f1d16ec1..ce812cf7fee6 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 @@ -58,6 +58,7 @@ import com.nextcloud.utils.ShortcutUtil; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; +import com.nmc.android.marketTracking.TrackingScanInterface; import com.nextcloud.utils.view.FastScrollUtils; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -77,6 +78,7 @@ import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation; import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; +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; @@ -231,6 +233,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, @@ -238,6 +247,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<>(); @@ -595,6 +606,24 @@ public void createRichWorkspace() { }).start(); } + @Override + public void scanDocument() { + //remote path to store the scans in the selected path + String remotePath = ""; + if (getActivity() != null && ((FileActivity) getActivity()).getCurrentDir() != null){ + remotePath = ((FileActivity) getActivity()).getCurrentDir().getRemotePath(); + } + + //remote path used so that user can directly save at the selected sub folder location + ScanActivity.openScanActivity(getActivity(), remotePath, 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()) { @@ -1301,6 +1330,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; @@ -1360,12 +1395,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 4ee3e691b7a1..58f7d7f1e4e4 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 @@ -976,6 +976,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 184517f4a8c8..b03dc04d74bc 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 @@ -33,6 +33,7 @@ public final class NotificationUtils { public static final String NOTIFICATION_CHANNEL_FILE_SYNC = "NOTIFICATION_CHANNEL_FILE_SYNC"; 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_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..596cfdbc3e91 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,18 +136,50 @@ - + + + + + + + + + - - + + + + + + + + + \ 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 cc434e6124dc..35f908e81cce 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 e6a298d76655..7704d96b38f7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -118,6 +118,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! @@ -147,6 +148,7 @@ Löschen Umbenennen Speichern + Auswählen Senden Teilen Überspringen @@ -1002,6 +1004,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 Bitte aktualisieren Sie die Android System WebView-App für eine Anmeldung diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 1ce3f0da4f73..2f79b79712ed 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -36,4 +36,68 @@ #1E1E1E @android:color/white + + + #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 89ed00a08bf2..46992e0a67c6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -75,4 +75,93 @@ @android:color/white #666666 #A5A5A5 + + + #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 e347e81c90d6..07b9641b139b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1092,6 +1092,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 @@ -1123,6 +1124,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