diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c89f2310843b..6b982f07cc3b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -192,7 +192,7 @@ android { // adapt structure from Eclipse to Gradle/Android Studio expectations; // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure packaging.resources { - excludes.addAll(listOf("META-INF/LICENSE*", "META-INF/versions/9/OSGI-INF/MANIFEST*")) + excludes.addAll(listOf("META-INF/LICENSE*", "META-INF/versions/9/OSGI-INF/MANIFEST*", "META-INF/DEPENDENCIES")) pickFirsts.add("MANIFEST.MF") // workaround for duplicated manifest on some dependencies } @@ -428,10 +428,11 @@ dependencies { implementation(libs.emoji.google) // endregion + // NextCloud scan is not required in NMC // region AppScan, 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")) // endregion // region SpotBugs @@ -518,6 +519,15 @@ dependencies { implementation(libs.coil) // endregion + // region scanbot sdk + // scanbot sdk: https://github.com/doo/scanbot-sdk-example-android + implementation(libs.scanbot.sdk) + // apache pdf-box for encrypting pdf files + implementation("org.apache.pdfbox:pdfbox:2.0.1") { + exclude(group = "commons-logging") + } + // endregion + // kotlinx.serialization implementation(libs.kotlinx.serialization.json) } diff --git a/app/src/androidTest/java/com/nmc/android/ScansResourceTest.kt b/app/src/androidTest/java/com/nmc/android/ScansResourceTest.kt new file mode 100644 index 000000000000..f4e9fe83166c --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ScansResourceTest.kt @@ -0,0 +1,422 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nmc.android + +import android.content.Context +import android.content.res.Configuration +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.owncloud.android.R +import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertArrayEquals +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Locale + +/** + * Test class to verify the strings and attrs customized in this branch PR for NMC + */ +@RunWith(AndroidJUnit4::class) +class ScansResourceTest { + + private val baseContext = ApplicationProvider.getApplicationContext() + + private val localizedStringMap = mapOf( + R.string.upload_scan_document to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Scan Document", + Locale.GERMAN to "Dokument scannen" + ) + ), + R.string.result_scan_doc_dont_move to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Do not move", + Locale.GERMAN to "Nicht bewegen" + ) + ), + R.string.result_scan_doc_move_closer to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Move closer", + Locale.GERMAN to "Näher heranbewegen" + ) + ), + R.string.result_scan_doc_perspective to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Perspective", + Locale.GERMAN to "Perspektive" + ) + ), + R.string.result_scan_doc_no_doc to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "No document", + Locale.GERMAN to "Kein Dokument" + ) + ), + R.string.result_scan_doc_bg_noisy to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Background too noisy", + Locale.GERMAN to "Hintergrund zu unruhig" + ) + ), + R.string.result_scan_doc_aspect_ratio to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Wrong aspect ratio.\nRotate your device.", + Locale.GERMAN to "Falsches Bildformat.\nDrehen Sie Ihr Gerät." + ) + ), + R.string.result_scan_doc_poor_light to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Poor light", + Locale.GERMAN to "Schwaches Licht" + ) + ), + R.string.scanned_doc_count to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "%d of %d", + Locale.GERMAN to "%d von %d" + ) + ), + R.string.title_edit_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Edit Scan", + Locale.GERMAN to "Scan bearbeiten" + ) + ), + R.string.title_crop_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Crop Scan", + Locale.GERMAN to "Scan beschneiden" + ) + ), + R.string.crop_btn_reset_crop_text to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Reset Crop", + Locale.GERMAN to "Rahmen zurücksetzen" + ) + ), + R.string.crop_btn_detect_doc_text to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Detect Document", + Locale.GERMAN to "Dokument erkennen" + ) + ), + R.string.edit_scan_filter_dialog_title to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Apply Filter", + Locale.GERMAN to "Filter anwenden" + ) + ), + R.string.edit_scan_filter_none to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "No Filter", + Locale.GERMAN to "Kein Filter" + ) + ), + R.string.edit_scan_filter_pure_binarized to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Whiteboard", + Locale.GERMAN to "Whiteboard" + ) + ), + R.string.edit_scan_filter_color_enhanced to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Photo Filter", + Locale.GERMAN to "Foto Filter" + ) + ), + R.string.edit_scan_filter_b_n_w to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Black & White", + Locale.GERMAN to "Schwarz-Weiß" + ) + ), + R.string.edit_scan_filter_color_document to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Document Filter", + Locale.GERMAN to "Dokument Filter" + ) + ), + R.string.edit_scan_filter_grey to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Grayscale", + Locale.GERMAN to "Grau" + ) + ), + R.string.automatic to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Automatic", + Locale.GERMAN to "Automatisch" + ) + ), + R.string.flash to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Flash", + Locale.GERMAN to "Blitz" + ) + ), + R.string.title_save_as to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Save as", + Locale.GERMAN to "Speichern unter" + ) + ), + R.string.scan_save_filename to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Filename", + Locale.GERMAN to "Dateiname" + ) + ), + R.string.scan_save_location to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Location", + Locale.GERMAN to "Speicherort" + ) + ), + R.string.scan_save_location_root to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "/Root folder", + Locale.GERMAN to "/Hauptverzeichnis" + ) + ), + R.string.scan_save_file_type to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "File type", + Locale.GERMAN to "Dateityp" + ) + ), + R.string.scan_save_without_text_recognition to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Save without text recognition", + Locale.GERMAN to "Speichern ohne Texterkennung" + ) + ), + R.string.scan_save_with_text_recognition to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Save with text recognition", + Locale.GERMAN to "Speichern mit Texterkennung" + ) + ), + R.string.scan_save_file_type_txt to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Textfile (txt)", + Locale.GERMAN to "Textdokument (txt)" + ) + ), + R.string.scan_save_pdf_password to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "PDF-Password", + Locale.GERMAN to "PDF-Passwort" + ) + ), + R.string.scan_save_set_password_hint to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Set password", + Locale.GERMAN to "Passwort setzen" + ) + ), + R.string.scan_save_no_file_select_toast to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Please select at least one filetype", + Locale.GERMAN to "Bitten wählen sie mindestens einen Dateityp zum Speichern aus." + ) + ), + R.string.save_scan_empty_pdf_password to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Please enter a password for the PDF you want to create or disable the function.", + Locale.GERMAN to "Bitte geben Sie ein Passwort für das zu erstellende PDF ein oder deaktivieren Sie die Funktion." + ) + ), + R.string.scan_save_file_type_text to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "You can save the file with or without text recognition. Multiple selection is allowed.", + Locale.GERMAN to "Sie können die gescannten Dokumente mit oder ohne Texterkennung abspeichern. Sie können auch mehrere Dateiformate auswählen." + ) + ), + R.string.camera_permission_denied to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "You cannot scan document without camera permission.", + Locale.GERMAN to "Sie können keine Dokumente scannen ohne die Erlaubnis die Kamera zu verwenden." + ) + ), + R.string.description_add_more_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Add more document", + Locale.GERMAN to "Weiteres Dokument hinzufügen" + ) + ), + R.string.description_crop_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Crop scanned document", + Locale.GERMAN to "Gescanntes Dokument zuschneiden" + ) + ), + R.string.description_filter_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Filter scanned document", + Locale.GERMAN to "Gescanntes Dokument filtern" + ) + ), + R.string.description_rotate_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Rotate scanned document", + Locale.GERMAN to "Gescanntes Dokument drehen" + ) + ), + R.string.description_delete_scan to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Delete scanned document", + Locale.GERMAN to "Gescanntes Dokument löschen" + ) + ), + R.string.description_edit_filename to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Edit scan filename", + Locale.GERMAN to "Scan-Dateinamen bearbeiten" + ) + ), + R.string.description_edit_location to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Edit scan location", + Locale.GERMAN to "Scan-Speicherort bearbeiten" + ) + ), + R.string.dialog_ok to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Ok", + Locale.GERMAN to "Ok" + ) + ), + R.string.dialog_save_scan_message to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Saving will take some time, especially if you have selected several pages and file formats.", + Locale.GERMAN to "Das Speichern kann einige Minuten in Anspruch nehmen, insbesondere wenn Sie mehrere Seiten und Dateiformate ausgewählt haben." + ) + ), + R.string.scan_save_file_type_pdf to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "PDF", + Locale.GERMAN to "PDF" + ) + ), + R.string.scan_save_file_type_jpg to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "JPG", + Locale.GERMAN to "JPG" + ) + ), + R.string.scan_save_file_type_png to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "PNG", + Locale.GERMAN to "PNG" + ) + ), + R.string.scan_save_file_type_pdf_ocr to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "PDF (OCR)", + Locale.GERMAN to "PDF (OCR)" + ) + ), + R.string.foreground_service_save to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Saving files…", + Locale.GERMAN to "Dateien werden gespeichert…" + ) + ), + R.string.notification_channel_image_save to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Image save notification channel", + Locale.GERMAN to "Benachrichtigungskanal zum Speichern von Bildern" + ) + ), + R.string.notification_channel_image_save_description to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Shows image save progress", + Locale.GERMAN to "Zeigt den Fortschritt der Bildspeicherung an" + ) + ), + R.string.choose_location to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Choose location", + Locale.GERMAN to "Ort wählen" + ) + ), + R.string.common_select to ExpectedLocalizedString( + translations = mapOf( + Locale.ENGLISH to "Select", + Locale.GERMAN to "Auswählen" + ) + ) + ) + + @Test + fun verifyLocalizedStrings() { + localizedStringMap.forEach { (stringRes, expected) -> + expected.translations.forEach { (locale, expectedText) -> + + val config = Configuration(baseContext.resources.configuration) + config.setLocale(locale) + + val localizedContext = baseContext.createConfigurationContext(config) + val actualText = localizedContext.getString(stringRes) + + assertEquals( + "Mismatch for ${baseContext.resources.getResourceEntryName(stringRes)} in $locale", + expectedText, + actualText + ) + } + } + } + + data class ExpectedLocalizedString(val translations: Map) + + private val localizedStringArrayMap = mapOf( + R.array.edit_scan_filter_values to ExpectedLocalizedStringArray( + translations = mapOf( + Locale.ENGLISH to arrayOf( + "No Filter", + "Photo Filter", + "Document Filter", + "Grayscale", + "Black & White", + "Whiteboard" + ), + Locale.GERMAN to arrayOf( + "Kein Filter", + "Foto Filter", + "Dokument Filter", + "Grau", + "Schwarz-Weiß", + "Whiteboard" + ) + ) + ), + ) + + @Test + fun verifyLocalizedStringArray() { + localizedStringArrayMap.forEach { (arrayRes, expected) -> + expected.translations.forEach { (locale, expectedArray) -> + + val config = Configuration(baseContext.resources.configuration) + config.setLocale(locale) + + val localizedContext = baseContext.createConfigurationContext(config) + val actualArray = localizedContext.resources.getStringArray(arrayRes) + + assertArrayEquals( + "Mismatch for ${baseContext.resources.getResourceEntryName(arrayRes)} in $locale", + expectedArray, + actualArray + ) + } + } + } + + data class ExpectedLocalizedStringArray(val translations: Map>) +} 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.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt index 9f950eac778a..1415f21fb3ae 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt @@ -433,6 +433,7 @@ class DialogFragmentIT : AbstractIT() { override fun createFolder() = Unit override fun uploadFromApp() = Unit override fun uploadFiles() = Unit + override fun scanDocument() = Unit override fun newDocument() = Unit override fun newSpreadsheet() = Unit override fun newPresentation() = Unit diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt index 628c28188e4b..2d0553f515cd 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt @@ -380,13 +380,17 @@ class OCFileListFragmentStaticServerIT : AbstractIT() { testFolder.richWorkspace = " " activity.storageManager.saveFile(testFolder) - sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") + sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "", + showOnlyFolder = false, + hideEncryptedFolder = false) Assert.assertFalse(sut.adapter.shouldShowHeader()) testFolder.richWorkspace = null activity.storageManager.saveFile(testFolder) - sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") + sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "", + showOnlyFolder = false, + hideEncryptedFolder = false) Assert.assertFalse(sut.adapter.shouldShowHeader()) EspressoIdlingResource.increment() 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 627cb92a06ef..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 @@ -17,7 +16,7 @@ import dagger.Reusable internal class VariantModule { @Provides @Reusable - fun scanOptionalFeature(): AppScanOptionalFeature = object : AppScanOptionalFeature() { - override fun getScanContract() = ScanPageContract() + fun scanOptionalFeature(): AppScanOptionalFeature { + return AppScanOptionalFeature.Stub } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 940739adb944..b7a96213c111 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -109,6 +109,7 @@ + + + + diff --git a/app/src/main/assets/ocr_blobs/deu.traineddata b/app/src/main/assets/ocr_blobs/deu.traineddata new file mode 100644 index 000000000000..97ed7b2b60f2 Binary files /dev/null and b/app/src/main/assets/ocr_blobs/deu.traineddata differ diff --git a/app/src/main/assets/ocr_blobs/eng.traineddata b/app/src/main/assets/ocr_blobs/eng.traineddata new file mode 100644 index 000000000000..bbef4675053b Binary files /dev/null and b/app/src/main/assets/ocr_blobs/eng.traineddata differ diff --git a/app/src/main/assets/ocr_blobs/osd.traineddata b/app/src/main/assets/ocr_blobs/osd.traineddata new file mode 100644 index 000000000000..183644aa5754 Binary files /dev/null and b/app/src/main/assets/ocr_blobs/osd.traineddata differ diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index dc7194f78b0a..2048584e5eff 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -36,6 +36,8 @@ import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet; 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; @@ -459,6 +461,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 b7d06ee2d9d3..e1e370fcac97 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -32,6 +32,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 @@ -98,6 +99,7 @@ class BackgroundJobFactory @Inject constructor( FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) + ScanDocUploadWorker::class -> createScanDocUploadWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters) @@ -261,6 +263,15 @@ class BackgroundJobFactory @Inject constructor( params = params ) + private fun createScanDocUploadWork(context: Context, params: WorkerParameters): ScanDocUploadWorker { + return ScanDocUploadWorker( + context = context, + params = params, + notificationManager, + accountManager + ) + } + private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork = HealthStatusWork( context, params, diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 219de803ecda..33a3114263f1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -157,6 +157,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() @@ -173,4 +180,6 @@ interface BackgroundJobManager { fun startMetadataSyncJob(currentDirPath: String) fun downloadFolder(folder: OCFile, accountName: String) fun cancelFolderDownload() + + 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 ed9aaefa6eff..2eabca4a1b48 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -23,6 +23,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 @@ -43,8 +44,10 @@ import com.owncloud.android.operations.DownloadType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +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 @@ -93,6 +96,8 @@ internal class BackgroundJobManagerImpl( const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" const val JOB_OFFLINE_OPERATIONS = "offline_operations" const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations" + const val JOB_IMMEDIATE_SCAN_DOC_UPLOAD = "immediate_scan_doc_upload" + const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" const val JOB_DOWNLOAD_FOLDER = "download_folder" @@ -769,6 +774,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) @@ -851,4 +876,21 @@ internal class BackgroundJobManagerImpl( override fun cancelFolderDownload() { workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER) } + + 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 699b2d927e87..a92c5b2bd215 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -350,6 +350,15 @@ default void onDarkThemeModeChanged(DarkMode mode) { long getPhotoSearchTimestamp(); + /** + * 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 3d6330923ec6..e4f6d004a274 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; @@ -82,6 +83,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"; @@ -708,6 +710,16 @@ private static String getKeyFromFolder(String preferenceName, @Nullable OCFile f return preferenceName + "_" + folderIdString; } + @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..4501e53c9829 --- /dev/null +++ b/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt @@ -0,0 +1,286 @@ +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.core.net.toFile +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.pdf.model.PageSize +import io.scanbot.pdf.model.PdfConfiguration +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.docprocessing.Document +import io.scanbot.sdk.docprocessing.Page +import io.scanbot.sdk.ocr.OcrEngine +import io.scanbot.sdk.ocr.process.OcrResult +import io.scanbot.sdk.process.PdfGenerator +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 + +// migration guide links +// PdfGenerator: https://github.com/doo/scanbot-sdk-example-android/tree/master/classic-components-example/pdf-generation +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: PdfGenerator + private lateinit var opticalCharacterRecognizer: OcrEngine + 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.createPdfGenerator() + opticalCharacterRecognizer = scanbotSDK.createOcrEngine() + } + + 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 outputFile = FileUtils.getPdfFileName(context, fileName) + val isSuccess = pdfRenderer.generateFromDocument( + pageList, + outputFile, + PdfConfiguration.default().copy(pageSize = PageSize.A4) + ) + + if (isSuccess) { + outputFile?.let { file -> + file.parent?.let { parent -> + val renamedFile = File(parent + OCFile.PATH_SEPARATOR + fileName + ".pdf") + if (file.renameTo(renamedFile)) { + Log_OC.d(TAG, "File successfully renamed") + } + savePdfFile(pdfPassword, renamedFile) + } + } + } else { + Log_OC.e(TAG, "Failed to create NonOCR PDF File") + } + } + + /** + * 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): Document { + val pageList: MutableList = ArrayList() + val document = scanbotSDK.documentApi.createDocument() + for (bitmap in bitmapList) { + val page = document.addPage(bitmap) + pageList.add(page) + } + return document + } + + private fun savePDFWithOCR(fileName: String?, bitmapList: List, pdfPassword: String?) { + val document = getScannedPages(bitmapList) + val isSuccess = + pdfRenderer.generateWithOcrFromDocument( + document, + PdfConfiguration.default().copy(pageSize = PageSize.A4) + ) + /*val ocrPageList: List = ocrResult.ocrPages + if (ocrPageList.isNotEmpty()) { + val ocrText = ocrResult.recognizedText + }*/ + if (isSuccess) { + val ocrPDFFile = document.pdfUri.toFile() + ocrPDFFile.parent?.let { parent -> + val renamedFile = File(parent + OCFile.PATH_SEPARATOR + fileName + "_OCR.pdf") + if (ocrPDFFile.renameTo(renamedFile)) { + Log_OC.d(TAG, "OCR File successfully renamed") + } + savePdfFile(pdfPassword, renamedFile) + } + } else { + Log_OC.e(TAG, "Failed to create OCR PDF File") + } + } + + 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 ocrResult: OcrResult = opticalCharacterRecognizer.recognizeFromBitmap(bitmap) + 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..3b9319b453d3 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/CropScannedDocumentFragment.kt @@ -0,0 +1,349 @@ +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.Build +import android.os.Bundle +import android.util.Pair +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.lifecycle.lifecycleScope +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.common.LineSegmentFloat +import io.scanbot.sdk.document.DocumentDetectionStatus +import io.scanbot.sdk.document.DocumentScanner +import io.scanbot.sdk.process.ImageProcessor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +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 documentScanner: DocumentScanner + + private var scannedDocIndex: Int = -1 + private lateinit var originalBitmap: Bitmap + private lateinit var previewBitmap: 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 (_: 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 + documentScanner = scanbotSDK.createDocumentScanner() + + 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() { + lifecycleScope.launch { + val initImageResult = withContext(Dispatchers.Default) { + val scannedDocs = onDocScanListener.getScannedDocs() + // NMC-3614 fix + if (scannedDocs.isEmpty()) { + return@withContext InitImageResult(Pair(listOf(), listOf()), listOf()) + } + + originalBitmap = scannedDocs[scannedDocIndex] + previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap) + + val result = documentScanner.scanFromBitmap(originalBitmap) + return@withContext when (result?.status) { + DocumentDetectionStatus.OK, + DocumentDetectionStatus.OK_BUT_BAD_ANGLES, + DocumentDetectionStatus.OK_BUT_TOO_SMALL, + DocumentDetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + val linesPair = Pair(result.horizontalLinesNormalized, result.verticalLinesNormalized) + val polygon = result.pointsNormalized + + InitImageResult(linesPair, polygon) + } + + else -> InitImageResult(Pair(listOf(), listOf()), listOf()) + } + } + + withContext(Dispatchers.Main) { + 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() + } + } + } + } + + private fun crop() { + // crop & warp image by selected polygon (editPolygonView.getPolygon()) + var documentImage = ImageProcessor(originalBitmap).crop(binding.cropPolygonView.polygon).processedBitmap() + 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 + } + } + + internal inner class InitImageResult( + val linesPair: Pair, List>, + val polygon: List + ) +} \ 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..722ffed2f49b --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/EditScannedDocumentFragment.kt @@ -0,0 +1,219 @@ +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 + +// migration guide link +// edit polygon view: https://github.com/doo/scanbot-sdk-example-android/tree/master/classic-components-example/edit-polygon-view +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 (_: 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.filteringState == ScanPagerFragment.FilteringState.IDLE) { + 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..39dce99db478 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/SaveScannedDocumentFragment.kt @@ -0,0 +1,384 @@ +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.getDecryptedPath +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nmc.android.utils.CheckableThemeUtils.tintCheckbox +import com.nmc.android.utils.CheckableThemeUtils.tintSwitch +import com.nmc.android.utils.FileUtils +import com.nmc.android.utils.KeyboardUtils +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentScanSaveBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.FolderPickerActivity +import com.owncloud.android.utils.DisplayUtils +import javax.inject.Inject + +class SaveScannedDocumentFragment : Fragment(), CompoundButton.OnCheckedChangeListener, Injectable, + View.OnClickListener { + + private lateinit var binding: FragmentScanSaveBinding + + private var isFileNameEditable = false + private var remotePath: String = "/" + private var remoteFilePath: OCFile? = null + + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + + @Inject + lateinit var appPreferences: AppPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Fragment screen orientation normal both portrait and landscape + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + retainInstance = true + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(true) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(true) + (requireActivity() as ScanActivity).updateActionBarTitleAndHomeButtonByString( + resources.getString(R.string.title_save_as) + ) + } + binding = FragmentScanSaveBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViews() + prepareRemotePath() + implementCheckListeners() + implementClickEvent() + } + + private fun implementClickEvent() { + binding.scanSaveFilenameInputEditBtn.setOnClickListener(this) + binding.scanSaveLocationEditBtn.setOnClickListener(this) + binding.saveScanBtnCancel.setOnClickListener(this) + binding.saveScanBtnSave.setOnClickListener(this) + } + + /** + * prepare remote path to save scanned files + */ + private fun prepareRemotePath() { + //check if user has selected scan document from sub folders + //if yes then show that folder in location to save scanned documents + //else check in preferences for last selected path + //if no last path selected available then show default /Scans/ path + + if (requireActivity() is ScanActivity) { + val remoteFile = (requireActivity() as ScanActivity).remoteFile + val remotePath = remoteFile?.remotePath + //remote path should not be null and should not be root path i.e only / + if (remotePath != null && remotePath != OCFile.ROOT_PATH) { + setRemoteFilePath(remoteFile) + return + } + + val lastRemotePath = appPreferences.uploadScansLastPath + //if user coming from Root path and the last saved path is not Scans folder + //then show the Root as scan doc path + if (remotePath == OCFile.ROOT_PATH && lastRemotePath != ScanActivity.DEFAULT_UPLOAD_SCAN_PATH) { + setRemoteFilePath(remoteFile) + return + } + } + + setRemoteFilePath(appPreferences.uploadScansLastPath) + } + + fun setRemoteFilePath(remotePath: String) { + remoteFilePath = OCFile(remotePath) + remoteFilePath?.setFolder() + // NMC-3512 fix + // here the file will not have the fileId which will fail to validate if the folder is root or any other folder + // so we have to fetch it from storage manager + if (remoteFilePath?.fileId == -1L) { + remoteFilePath = (requireActivity() as ScanActivity).storageManager.getFileByDecryptedRemotePath(remotePath) + } + + updateSaveLocationText() + } + + private fun setRemoteFilePath(remoteFile: OCFile) { + remoteFilePath = remoteFile + + updateSaveLocationText() + } + + private fun initViews() { + binding.scanSaveFilenameInput.setText(FileUtils.scannedFileName()) + tintSwitch(binding.scanSavePdfPasswordSwitch) + tintCheckbox( + binding.scanSaveWithoutTxtRecognitionPdfCheckbox, + binding.scanSaveWithoutTxtRecognitionPngCheckbox, + binding.scanSaveWithoutTxtRecognitionJpgCheckbox, + binding.scanSaveWithTxtRecognitionPdfCheckbox, + binding.scanSaveWithTxtRecognitionTxtCheckbox + ) + binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked = true + binding.scanSavePdfPasswordTextInput.defaultHintTextColor = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_focused), + intArrayOf(android.R.attr.state_focused), + ), + intArrayOf( + Color.GRAY, + resources.getColor(R.color.text_color, null) + ) + ) + } + + private fun implementCheckListeners() { + binding.scanSaveWithoutTxtRecognitionPdfCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithoutTxtRecognitionJpgCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithoutTxtRecognitionPngCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithTxtRecognitionPdfCheckbox.setOnCheckedChangeListener(this) + binding.scanSaveWithTxtRecognitionTxtCheckbox.setOnCheckedChangeListener(this) + binding.scanSavePdfPasswordSwitch.setOnCheckedChangeListener(this) + + binding.scanSaveFilenameInput.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + enableFileNameEditing() + return@setOnEditorActionListener true + } + false + } + } + + private fun enableDisablePdfPasswordSwitch() { + binding.scanSavePdfPasswordSwitch.isEnabled = + binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked || binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked + if (!binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked && !binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked) { + binding.scanSavePdfPasswordSwitch.isChecked = false + } + } + + private fun showHidePdfPasswordInput(isChecked: Boolean) { + binding.scanSavePdfPasswordTextInput.visibility = if (isChecked) View.VISIBLE else View.GONE + if (isChecked) { + binding.scanSaveNestedScrollView.post { binding.scanSaveNestedScrollView.fullScroll(ScrollView.FOCUS_DOWN) } + } + if (isChecked) { + KeyboardUtils.showSoftKeyboard(requireContext(), binding.scanSavePdfPasswordEt) + } else { + KeyboardUtils.hideKeyboardFrom(requireContext(), binding.scanSavePdfPasswordEt) + } + } + + private fun enableFileNameEditing() { + isFileNameEditable = !isFileNameEditable + binding.scanSaveFilenameInput.isEnabled = isFileNameEditable + if (isFileNameEditable) { + binding.scanSaveFilenameInputEditBtn.setImageResource(R.drawable.ic_tick) + KeyboardUtils.showSoftKeyboard(requireContext(), binding.scanSaveFilenameInput) + binding.scanSaveFilenameInput.setSelection( + binding.scanSaveFilenameInput.text.toString().trim { it <= ' ' }.length + ) + } else { + binding.scanSaveFilenameInputEditBtn.setImageResource(R.drawable.ic_pencil_edit) + KeyboardUtils.hideKeyboardFrom(requireContext(), binding.scanSaveFilenameInput) + } + } + + override fun onClick(view: View) { + when (view.id) { + R.id.scan_save_filename_input_edit_btn -> enableFileNameEditing() + R.id.scan_save_location_edit_btn -> { + val action = Intent(requireActivity(), FolderPickerActivity::class.java) + action.putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION) + action.putExtra(FolderPickerActivity.EXTRA_SHOW_ONLY_FOLDER, true) + action.putExtra(FolderPickerActivity.EXTRA_HIDE_ENCRYPTED_FOLDER, false) + scanDocSavePathResultLauncher.launch(action) + } + + R.id.save_scan_btn_cancel -> requireActivity().onBackPressedDispatcher.onBackPressed() + R.id.save_scan_btn_save -> saveScannedFiles() + } + } + + private fun saveScannedFiles() { + val fileName = binding.scanSaveFilenameInput.text.toString().trim { it <= ' ' } + if (TextUtils.isEmpty(fileName)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty) + return + } + + if (!com.owncloud.android.lib.resources.files.FileUtils.isValidName(fileName)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server) + return + } + + if (!binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked + && !binding.scanSaveWithoutTxtRecognitionJpgCheckbox.isChecked + && !binding.scanSaveWithoutTxtRecognitionPngCheckbox.isChecked + && !binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked + && !binding.scanSaveWithTxtRecognitionTxtCheckbox.isChecked + ) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.scan_save_no_file_select_toast) + return + } + + val fileTypesStringBuilder = StringBuilder() + if (binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_PDF) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithoutTxtRecognitionJpgCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_JPG) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithoutTxtRecognitionPngCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_PNG) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_PDF_OCR) + fileTypesStringBuilder.append(",") + } + if (binding.scanSaveWithTxtRecognitionTxtCheckbox.isChecked) { + fileTypesStringBuilder.append(SAVE_TYPE_TXT) + } + val pdfPassword = binding.scanSavePdfPasswordEt.text.toString().trim { it <= ' ' } + if (binding.scanSavePdfPasswordSwitch.isChecked && TextUtils.isEmpty(pdfPassword)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.save_scan_empty_pdf_password) + return + } + + // NMC-3670 + if (requireActivity() is ScanActivity) { + remoteFilePath?.let { + (requireActivity() as ScanActivity).checkEncryption(it) { success -> + if (success) { + showPromptToSave(fileName, fileTypesStringBuilder, pdfPassword) + } + } + } + } + } + + private fun showPromptToSave(fileName: String, fileTypesStringBuilder: StringBuilder, pdfPassword: String) { + try { + val alertDialog = AlertDialog.Builder(requireContext()) + .setMessage(R.string.dialog_save_scan_message) + .setPositiveButton(R.string.dialog_ok) { _: DialogInterface?, _: Int -> + startSaving( + fileName, + fileTypesStringBuilder, pdfPassword + ) + } + .create() + + alertDialog.show() + } catch (e: BadTokenException) { + Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.message) + } + } + + private fun startSaving(fileName: String, fileTypesStringBuilder: StringBuilder, pdfPassword: String) { + //start the save and upload worker + backgroundJobManager.scheduleImmediateScanDocUploadJob( + fileTypesStringBuilder.toString(), + fileName, + remotePath, + pdfPassword + ) + + //save the selected location to save scans in preference + appPreferences.uploadScansLastPath = remotePath + + //send the result back with the selected remote path to open selected remote path + val intent = Intent() + val bundle = Bundle() + bundle.putParcelable(EXTRA_SCAN_DOC_REMOTE_PATH, remoteFilePath) + intent.putExtras(bundle) + requireActivity().setResult(Activity.RESULT_OK, intent) + requireActivity().finish() + } + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + when (buttonView.id) { + R.id.scan_save_without_txt_recognition_pdf_checkbox, R.id.scan_save_with_txt_recognition_pdf_checkbox -> enableDisablePdfPasswordSwitch() + R.id.scan_save_pdf_password_switch -> showHidePdfPasswordInput(isChecked) + } + } + + private fun updateSaveLocationText() { + // to upload the scan docs use remote path + remotePath = remoteFilePath?.remotePath ?: OCFile.ROOT_PATH + + // to show file path use decrypted path + var filePathToShow = + remoteFilePath?.let { (requireActivity() as ScanActivity).storageManager.getDecryptedPath(it) } + ?: OCFile.ROOT_PATH + if (filePathToShow.isEmpty() || filePathToShow.equals(OCFile.ROOT_PATH, ignoreCase = true)) { + filePathToShow = resources.getString(R.string.scan_save_location_root) + } + binding.scanSaveLocationInput.text = filePathToShow + } + + private var scanDocSavePathResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + // There are no request codes + val data: Intent? = result.data + if (data != null) { + val chosenFolder = FolderPickerActivity.EXTRA_FOLDER?.let { + data.getParcelableArgument(FolderPickerActivity.EXTRA_FOLDER, OCFile::class.java) + } + if (chosenFolder != null) { + remoteFilePath = chosenFolder + updateSaveLocationText() + } + } + } + } + + companion object { + private const val TAG: String = "SaveScannedDocumentFragment" + + fun newInstance(): SaveScannedDocumentFragment { + val args = Bundle() + val fragment = SaveScannedDocumentFragment() + fragment.arguments = args + return fragment + } + + const val SAVE_TYPE_PDF: String = "pdf" + const val SAVE_TYPE_PNG: String = "png" + const val SAVE_TYPE_JPG: String = "jpg" + const val SAVE_TYPE_PDF_OCR: String = "pdf_ocr" + const val SAVE_TYPE_TXT: String = "txt" + + const val EXTRA_SCAN_DOC_REMOTE_PATH: String = "scan_doc_remote_path" + } +} diff --git a/app/src/main/java/com/nmc/android/scans/ScanActivity.kt b/app/src/main/java/com/nmc/android/scans/ScanActivity.kt new file mode 100644 index 000000000000..78b7eaae079d --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanActivity.kt @@ -0,0 +1,303 @@ +package com.nmc.android.scans + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import android.text.TextUtils +import android.view.MenuItem +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.nmc.android.scans.ScanDocumentFragment.Companion.newInstance +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityScanBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.operations.CreateFolderIfNotExistOperation +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment +import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.utils.DisplayUtils +import io.scanbot.sdk.ScanbotSDK +import androidx.core.graphics.drawable.toDrawable + +class ScanActivity : FileActivity(), OnFragmentChangeListener, OnDocScanListener { + private lateinit var binding: ActivityScanBinding + lateinit var scanbotSDK: ScanbotSDK + + var remoteFile: OCFile? = null + private set + + // flag to avoid checking folder existence whenever user goes to save fragment + // we will make it true when the operation finishes first time + private var isFolderCheckOperationFinished = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Inflate and set the layout view + binding = ActivityScanBinding.inflate(layoutInflater) + setContentView(binding.root) + remoteFile = intent.getParcelableArgument(EXTRA_REMOTE_PATH, OCFile::class.java) + originalScannedImages.clear() + filteredImages.clear() + scannedImagesFilterIndex.clear() + initScanbotSDK() + setupToolbar() + setupActionBar() + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + private fun setupActionBar() { + val actionBar = delegate.supportActionBar + actionBar?.let { + it.setBackgroundDrawable(resources.getColor(R.color.bg_default, null).toDrawable()) + it.setDisplayHomeAsUpEnabled(true) + viewThemeUtils.files.themeActionBar(this, it, false) + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + createScanFragment(savedInstanceState) + } + + private fun createScanFragment(savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + val scanDocumentFragment = newInstance(TAG) + onReplaceFragment(scanDocumentFragment, FRAGMENT_SCAN_TAG, false) + } else { + supportFragmentManager.findFragmentByTag(FRAGMENT_SCAN_TAG) + } + } + + override fun onReplaceFragment(fragment: Fragment, tag: String, addToBackStack: Boolean) { + // only during replacing save scan fragment + if (tag.equals(FRAGMENT_SAVE_SCAN_TAG, ignoreCase = true)) { + checkAndCreateFolderIfRequired() + } + + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.scan_frame_container, fragment, tag) + if (addToBackStack) { + transaction.addToBackStack(tag) + } + transaction.commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressHandle() + } + return super.onOptionsItemSelected(item) + } + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onBackPressHandle() + } + } + + private fun onBackPressHandle() { + val editScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_EDIT_SCAN_TAG) + val cropScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_CROP_SCAN_TAG) + val saveScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_SAVE_SCAN_TAG) + if (cropScanFragment != null || saveScanFragment != null) { + var index = 0 + if (cropScanFragment is CropScannedDocumentFragment) { + index = cropScanFragment.getScannedDocIndex() + } + onReplaceFragment(EditScannedDocumentFragment.newInstance(index), FRAGMENT_EDIT_SCAN_TAG, false) + } else if (editScanFragment != null) { + createScanFragment(null) + } else { + finish() + } + } + + private fun initScanbotSDK() { + scanbotSDK = ScanbotSDK(this) + } + + override fun addScannedDoc(file: Bitmap?) { + file?.let { + originalScannedImages.add(it) + filteredImages.add(it) + scannedImagesFilterIndex.add(0) // no filter by default + } + } + + override fun getScannedDocs(): List { + return filteredImages + } + + override fun removedScannedDoc(file: Bitmap?, index: Int): Boolean { + //removed the filter applied index also when scanned document is removed + if (scannedImagesFilterIndex.isNotEmpty() && scannedImagesFilterIndex.size > index) { + scannedImagesFilterIndex.removeAt(index) + } + if (originalScannedImages.isNotEmpty() && file != null) { + originalScannedImages.removeAt(index) + } + if (filteredImages.isNotEmpty() && 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.isNotEmpty() && newFile != null && index >= 0 && originalScannedImages.size - 1 >= index) { + originalScannedImages[index] = newFile + } + if (filteredImages.isNotEmpty() && newFile != null && index >= 0 && filteredImages.size - 1 >= index) { + return filteredImages.set(index, newFile) + } + return null + } + + override fun replaceFilterIndex(index: Int, filterIndex: Int) { + if (scannedImagesFilterIndex.isNotEmpty() && scannedImagesFilterIndex.size > index) { + scannedImagesFilterIndex[index] = filterIndex + } + } + + private fun checkAndCreateFolderIfRequired() { + val remotePath = remoteFile?.remotePath + + //if user is coming from sub-folder then we should not check for existence as folder will be available + if (!TextUtils.isEmpty(remotePath) && remotePath != OCFile.ROOT_PATH) { + return + } + + //no need to do any operation if its already finished earlier + if (isFolderCheckOperationFinished) { + return + } + + val lastRemotePath = appPreferences.uploadScansLastPath + + //create the default scan folder if it doesn't exist or if user has not selected any other folder + if (lastRemotePath.equals(DEFAULT_UPLOAD_SCAN_PATH, ignoreCase = true)) { + fileOperationsHelper.createFolderIfNotExist(lastRemotePath, false) + } + } + + // NMC-3670 + // check if selected folder is encrypted and e2ee is configured or not + fun checkEncryption(file: OCFile, resultListener: (success: Boolean) -> Unit) { + // get file from storage to have the encrypted information + // as we are making OCFile without the flag {see-> SaveScannedDocumentFragment.setRemoteFilePath()} + var remoteFile = storageManager.getFileByEncryptedRemotePath(file.remotePath) + // there can be case where the remoteFile can be null + if (remoteFile == null) { + remoteFile = file + } + + if (!remoteFile.isEncrypted) { + resultListener(true) + return + } + + if (remoteFile.isEncrypted) { + val user = user.orElseThrow { RuntimeException() } + + // check if e2e app is enabled + val ocCapability: OCCapability = storageManager + .getCapability(user.accountName) + + if (ocCapability.endToEndEncryption.isFalse || + ocCapability.endToEndEncryption.isUnknown + ) { + DisplayUtils.showSnackMessage(this, R.string.end_to_end_encryption_not_enabled) + resultListener(false) + return + } + // check if keys are stored + if (FileOperationsHelper.isEndToEndEncryptionSetup(this, user)) { + resultListener(true) + } else { + val setupEncryptionDialogFragment = SetupEncryptionDialogFragment.newInstance(user, -1) + supportFragmentManager.setFragmentResultListener( + SetupEncryptionDialogFragment.RESULT_REQUEST_KEY, + this + ) { requestKey, result -> + if (requestKey == SetupEncryptionDialogFragment.RESULT_REQUEST_KEY) { + resultListener( + !result.getBoolean(SetupEncryptionDialogFragment.RESULT_KEY_CANCELLED, false) + && result.getBoolean(SetupEncryptionDialogFragment.SUCCESS, false) + ) + } + } + setupEncryptionDialogFragment.show( + supportFragmentManager, + SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG + ) + } + } + } + + override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { + super.onRemoteOperationFinish(operation, result) + if (operation is CreateFolderIfNotExistOperation) { + //we are only handling callback when we are checking if folder exist or not to update the UI + //in case the folder doesn't exist (user has deleted) + if (!result.isSuccess && result.code == RemoteOperationResult.ResultCode.FILE_NOT_FOUND) { + val saveScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_SAVE_SCAN_TAG) + if (saveScanFragment != null && saveScanFragment.isVisible) { + //update the root path in preferences as well + //so that next time folder issue won't come + appPreferences.uploadScansLastPath = OCFile.ROOT_PATH + //if folder doesn't exist then we have to set the remote path as root i.e. fallback mechanism + (saveScanFragment as SaveScannedDocumentFragment).setRemoteFilePath(OCFile.ROOT_PATH) + } + } + // NMC-4746 fix + else if (result.isSuccess) { + val saveScanFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_SAVE_SCAN_TAG) + if (saveScanFragment != null && saveScanFragment.isVisible) { + // when folder creation is success update the path in save fragment + (saveScanFragment as SaveScannedDocumentFragment).setRemoteFilePath(appPreferences.uploadScansLastPath) + } + } + isFolderCheckOperationFinished = true + } + } + + companion object { + const val FRAGMENT_SCAN_TAG: String = "SCAN_FRAGMENT_TAG" + const val FRAGMENT_EDIT_SCAN_TAG: String = "EDIT_SCAN_FRAGMENT_TAG" + const val FRAGMENT_CROP_SCAN_TAG: String = "CROP_SCAN_FRAGMENT_TAG" + const val FRAGMENT_SAVE_SCAN_TAG: String = "SAVE_SCAN_FRAGMENT_TAG" + + // default path to upload the scanned document + // if user doesn't select any location then this will be the default location + const val DEFAULT_UPLOAD_SCAN_PATH: String = OCFile.ROOT_PATH + "Scans" + OCFile.PATH_SEPARATOR + + const val TAG: String = "ScanActivity" + private const val EXTRA_REMOTE_PATH = "com.nmc.android.scans.scan_activity.extras.remote_path" + + @JvmField + val originalScannedImages: MutableList = ArrayList() //list with original bitmaps + + @JvmField + val filteredImages: MutableList = ArrayList() //list with bitmaps applied filters + + @JvmField + val scannedImagesFilterIndex: MutableList = ArrayList() //list to maintain the state of + // applied filter index when device rotated + + @JvmStatic + fun openScanActivity(context: Context, remoteFile: OCFile, requestCode: Int) { + val intent = Intent(context, ScanActivity::class.java) + intent.putExtra(EXTRA_REMOTE_PATH, remoteFile) + (context as AppCompatActivity).startActivityForResult(intent, requestCode) + } + } +} diff --git a/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt b/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt new file mode 100644 index 000000000000..81893cd24e8d --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanDocumentFragment.kt @@ -0,0 +1,433 @@ +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.document.DocumentDetectionStatus +import io.scanbot.sdk.document.DocumentScanner +import io.scanbot.sdk.document.DocumentScannerFrameHandler +import io.scanbot.sdk.document.ui.IDocumentScannerViewCallback +import io.scanbot.sdk.ocr.OcrEngine +import io.scanbot.sdk.process.ImageProcessor +import io.scanbot.sdk.ui.view.base.configuration.CameraOrientationMode + +// migration guide links +// DocumentScanner: https://github.com/doo/scanbot-sdk-example-android/tree/master/classic-components-example/document-scanner +// OCREngine: https://github.com/doo/scanbot-sdk-example-android/tree/master/classic-components-example/ocr + +class ScanDocumentFragment : Fragment() { + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var documentScanner: DocumentScanner + + private var lastUserGuidanceHintTs = 0L + private var flashEnabled = false + private var autoSnappingEnabled = true + private val ignoreBadAspectRatio = true + + //OCR + private lateinit var opticalCharacterRecognizer: OcrEngine + + 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 (_: 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() + initScanningBehavior( + this@ScanDocumentFragment.documentScanner, + { 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) + setIgnoreOrientationMismatch(ignoreBadAspectRatio) + + // Please note: https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/autosnapping/#sensitivity + setAutoSnappingSensitivity(0.85f) + } + + binding.shutterButton.setOnClickListener { binding.camera.viewController.takePicture(false) } + binding.shutterButton.visibility = View.VISIBLE + + binding.scanDocBtnFlash.setOnClickListener { + flashEnabled = !flashEnabled + binding.camera.viewController.useFlash(flashEnabled) + toggleFlashButtonUI() + } + binding.scanDocBtnCancel.setOnClickListener { + // if fragment opened from Edit Scan Fragment then on cancel click it should go to that fragment + if (calledFrom == EditScannedDocumentFragment.TAG) { + openEditScanFragment() + } else { + // else default behaviour + (requireActivity() as ScanActivity).onBackPressed() + } + } + + binding.scanDocBtnAutomatic.setOnClickListener { + autoSnappingEnabled = !autoSnappingEnabled + setAutoSnapEnabled(autoSnappingEnabled) + } + binding.scanDocBtnAutomatic.post { setAutoSnapEnabled(autoSnappingEnabled) } + + toggleFlashButtonUI() + } + + private fun toggleFlashButtonUI() { + if (flashEnabled) { + binding.scanDocBtnFlash.setIconTintResource(R.color.primary) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) + } else { + binding.scanDocBtnFlash.setIconTintResource(R.color.grey_60) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) + } + } + + private fun askPermission() { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + requestMultiplePermissions.launch( + arrayOf( + Manifest.permission.CAMERA, + ) + ) + } + } + + private fun initDependencies() { + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + documentScanner = scanbotSDK.createDocumentScanner() + opticalCharacterRecognizer = scanbotSDK.createOcrEngine() + } + + 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: DocumentDetectionStatus) { + 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) { + DocumentDetectionStatus.OK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.OK_BUT_TOO_SMALL -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_move_closer) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.OK_BUT_BAD_ANGLES -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_perspective) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.ERROR_NOTHING_DETECTED -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_no_doc) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.ERROR_TOO_NOISY -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_bg_noisy) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.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 + } + + DocumentDetectionStatus.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 = documentScanner.scanFromBitmap(originalBitmap)!! + val detectedPolygon = result.pointsNormalized + + val documentImage = ImageProcessor(originalBitmap).crop(detectedPolygon).processedBitmap() + + // val file = saveImage(documentImage) + // Log.d("SCANNING","File : $file") + if (documentImage != null) { + onDocScanListener.addScannedDoc(documentImage) + // onDocScanListener.addScannedDoc(FileUtils.saveImage(requireContext(), documentImage, null)) + openEditScanFragment() + + /* uiScope.launch { + recognizeTextWithoutPDFTask(documentImage) + }*/ + } + // RecognizeTextWithoutPDFTask(documentImage).execute() + + //resultView.post { resultView.setImageBitmap(documentImage) } + + // continue scanning + /* cameraView.postDelayed({ + cameraView.continuousFocus() + cameraView.startPreview() + }, 1000)*/ + } + + private fun openEditScanFragment() { + onFragmentChangeListener.onReplaceFragment( + EditScannedDocumentFragment.newInstance(onDocScanListener.getScannedDocs().size - 1), + ScanActivity.FRAGMENT_EDIT_SCAN_TAG, false + ) + } + + private fun setAutoSnapEnabled(enabled: Boolean) { + binding.camera.viewController.apply { + autoSnappingEnabled = enabled + isFrameProcessingEnabled = enabled + } + binding.polygonView.visibility = if (enabled) View.VISIBLE else View.GONE + /*autoSnappingToggleButton.text = resources.getString(R.string.automatic) + " ${ + if (enabled) "ON" else + "OFF" + }"*/ + if (enabled) { + binding.scanDocBtnAutomatic.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) + binding.shutterButton.showAutoButton() + } else { + binding.scanDocBtnAutomatic.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) + binding.shutterButton.showManualButton() + binding.userGuidanceHint.visibility = View.GONE + } + } + + private val requestMultiplePermissions = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + permissions.entries.forEach { + if (!it.value) { + // permission not granted + val showRationale = shouldShowRequestPermissionRationale(it.key) + if (!showRationale) { + // user also CHECKED "never ask again" + // you can either enable some fall back, + // disable features of your app + // or open another dialog explaining + // again the permission and directing to + // the app setting + onPermissionDenied(requireActivity().resources.getString(R.string.camera_permission_rationale)) + } else if (Manifest.permission.CAMERA == it.key) { + // user did NOT check "never ask again" + // this is a good place to explain the user + // why you need the permission and ask if he wants + // to accept it (the rationale) + onPermissionDenied(requireActivity().resources.getString(R.string.camera_permission_denied)) + + // askPermission() + } + // else if ( /* possibly check more permissions...*/) { + // } + } + } + } + + private fun onPermissionDenied(message: String) { + // Show Toast instead of snackbar as we are finishing the activity + Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show() + requireActivity().finish() + } + + companion object { + + @JvmStatic + val ARG_CALLED_FROM = "arg_called_From" + + @JvmStatic + fun newInstance(calledFrom: String): ScanDocumentFragment { + val args = Bundle() + args.putString(ARG_CALLED_FROM, calledFrom) + val fragment = ScanDocumentFragment() + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/scans/ScanPagerFragment.kt b/app/src/main/java/com/nmc/android/scans/ScanPagerFragment.kt new file mode 100644 index 000000000000..35be438d2245 --- /dev/null +++ b/app/src/main/java/com/nmc/android/scans/ScanPagerFragment.kt @@ -0,0 +1,243 @@ +package com.nmc.android.scans + +import android.content.Context +import android.content.DialogInterface +import android.graphics.Bitmap +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +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.docprocessing.Document +import io.scanbot.sdk.docprocessing.Page +import io.scanbot.sdk.imagefilters.ColorDocumentFilter +import io.scanbot.sdk.imagefilters.GrayscaleFilter +import io.scanbot.sdk.imagefilters.LegacyFilter +import io.scanbot.sdk.imagefilters.ParametricFilter +import io.scanbot.sdk.process.ImageProcessor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +// migration guide links +// filters: https://github.com/doo/scanbot-sdk-example-android/tree/master/classic-components-example/adjustable-filters +class ScanPagerFragment : Fragment() { + private lateinit var binding: ItemScannedDocBinding + + private lateinit var scanbotSDK: ScanbotSDK + + private lateinit var document: Document + private lateinit var page: Page + + private var originalBitmap: Bitmap? = null + private var previewBitmap: Bitmap? = null + + private var lastRotationEventTs = 0L + private var rotationDegrees = 0 + private var index = 0 + + private var onDocScanListener: OnDocScanListener? = null + private var applyFilterDialog: AlertDialog? = null + private var selectedFilterIndex = 0 + var filteringState: FilteringState = FilteringState.IDLE + private var selectedFilter: List? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + try { + onDocScanListener = context as OnDocScanListener + } catch (_: 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) + lifecycleScope.launch { loadDocument() } + } + + private suspend fun loadDocument() { + val doc = withContext(Dispatchers.IO) { scanbotSDK.documentApi.createDocument() } + withContext(Dispatchers.Main) { + document = doc + + if (index >= 0 && index < ScanActivity.filteredImages.size) { + originalBitmap = onDocScanListener?.getScannedDocs()?.get(index) + originalBitmap?.let { + previewBitmap = resizeForPreview(it) + val page = document.addPage(it) + this@ScanPagerFragment.page = page + + val appliedFilter = page.filters.getOrNull(index) + selectedFilter = listOf(appliedFilter) + + } + } + if (index >= 0 && index < ScanActivity.scannedImagesFilterIndex.size) { + selectedFilterIndex = ScanActivity.scannedImagesFilterIndex[index] + } + + 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 { + lifecycleScope.launch { + withContext(Dispatchers.Default) { + val rotatedBitmap = ImageProcessor(it).rotate(rotationDegrees).processedBitmap() + 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, + selectedFilterIndex + ) { dialog: DialogInterface, which: Int -> + selectedFilterIndex = which + onDocScanListener?.replaceFilterIndex(index, selectedFilterIndex) + if (filterArray[which].equals(resources.getString(R.string.edit_scan_filter_none), ignoreCase = true)) { + applyFilter(null) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_pure_binarized), + ignoreCase = true + ) + ) { + // PURE_BINARIZED filter type in int + applyFilter(LegacyFilter(11)) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_color_enhanced), + ignoreCase = true + ) + ) { + // COLOR_ENHANCED & EDGE_HIGHLIGHT filter type in int + applyFilter(LegacyFilter(1), LegacyFilter(17)) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_color_document), + ignoreCase = true + ) + ) { + applyFilter(ColorDocumentFilter()) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_grey), + ignoreCase = true + ) + ) { + applyFilter(GrayscaleFilter()) + } else if (filterArray[which].equals( + resources.getString(R.string.edit_scan_filter_b_n_w), + ignoreCase = true + ) + ) { + // BLACK_AND_WHITE filter type in int + applyFilter(LegacyFilter(14)) + } + dialog.dismiss() + } + .setOnCancelListener { } + applyFilterDialog = builder.create() + applyFilterDialog?.show() + } + + private fun applyFilter(vararg imageFilterType: ParametricFilter?) { + if (selectedFilter == imageFilterType.toList()) { + return + } + + binding.editScanImageProgressBar.visibility = View.VISIBLE + selectedFilter = imageFilterType.toList() + originalBitmap?.let { + if (filteringState == FilteringState.IDLE) { + lifecycleScope.launch { + filteringState = FilteringState.PROCESSING + + withContext(Dispatchers.Default) { + // applying empty collection of filters will remove all filters + val filtersToApply = selectedFilter?.filterNotNull() + page.apply(newFilters = filtersToApply) + previewBitmap = page.documentPreviewImage + } + onDocScanListener?.replaceScannedDoc(index, previewBitmap, true) + + withContext(Dispatchers.Main) { + loadImage() + binding.editScanImageProgressBar.visibility = View.GONE + filteringState = FilteringState.IDLE + } + } + } else { + // already in progress + } + } + } + + 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 + } + } + + enum class FilteringState { + IDLE, + PROCESSING, + } +} 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..97496b9a5f59 --- /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")); + } + } + + public 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(".jpg", ".pdf")); + } + } + + 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..54df05db7a65 --- /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 until end of 2026 + const val LICENSE_KEY = "M7j1BCE/NVedJyWcstLjZvNQdeluHx" + + "XkMNsHYeuQ8o4MKhPITd/xJDsc9xfY" + + "JRPSCA5UpXbzVObI5MMeoFiUWMPCR6" + + "yoOe1Ghj1UjVIVS6lLW/Unipe+Pozm" + + "8TFO+l0Q0TAuWXXqwGZJt4dHy1t9t9" + + "QUy4i1q90VuVs1I0k4C3ScZNr2R+aT" + + "z4Hht5J5Svu4RwVPqcOiEuoAMYj8+a" + + "bvidW0CQK3+12ryaV64qzLrFtcHAb7" + + "Wx3aqZH7WXT/F4uZTYpaau6lzU+xIY" + + "YxtC8SS+6+nb2l6V2hIqmpEJwS1z0p" + + "uUbO7D7O5Gm3aSaOk+8xqX2mNuk4dX" + + "EyTSR36bFuVA==\nU2NhbmJvdFNESw" + + "pjb20udF9zeXN0ZW1zLmFuZHJvaWQu" + + "d2ViZGF2fGNvbS50X3N5c3RlbXMuYW" + + "5kcm9pZC53ZWJkYXYuYmV0YQoxODAz" + + "ODU5MTk5CjExNTU2NzgKMg==\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 e1b946e2781e..7bff3663c807 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -64,6 +64,7 @@ import com.nextcloud.utils.extensions.ContextExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.nmc.android.ui.LauncherActivity; +import com.nmc.android.utils.ScanBotSdkUtils; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -120,6 +121,8 @@ import dagger.android.DispatchingAndroidInjector; import dagger.android.HasAndroidInjector; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.scanbot.sap.SdkFeature; +import io.scanbot.sdk.ScanbotSDKInitializer; import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP; @@ -371,9 +374,9 @@ public void onCreate() { if (!MDMConfig.INSTANCE.sendFilesSupport(this)) { disableDocumentsStorageProvider(); } - - - } + + initialiseScanBotSDK(); + } public void disableDocumentsStorageProvider() { String packageName = getPackageName(); @@ -697,6 +700,10 @@ public static void notificationChannels() { R.string.notification_channel_content_observer_description, context, NotificationManager.IMPORTANCE_LOW); + + 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"); } @@ -845,7 +852,7 @@ private static void updateToAutoUpload(Context context) { } } - + private static void showAutoUploadAlertDialog(Context context) { new MaterialAlertDialogBuilder(context, R.style.Theme_ownCloud_Dialog) @@ -1015,4 +1022,24 @@ public void onTerminate() { super.onTerminate(); ReceiversHelper.shutdown(); } + + /** + * method to initialise the ScanBot SDK + */ + private void initialiseScanBotSDK() { + new ScanbotSDKInitializer() + .withLogging(BuildConfig.DEBUG, BuildConfig.DEBUG) + .license(this, ScanBotSdkUtils.LICENSE_KEY) + .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 6923606819af..0fb618934409 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; @@ -104,6 +105,7 @@ public class OperationsService extends Service { public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND"; public static final String EXTRA_FILES_DOWNLOAD_LIMIT = "FILES_DOWNLOAD_LIMIT"; public static final String EXTRA_SHARE_ATTRIBUTES = "SHARE_ATTRIBUTES"; + 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"; @@ -125,6 +127,7 @@ public class OperationsService extends Service { 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_UPDATE_FILES_DOWNLOAD_LIMIT = "UPDATE_FILES_DOWNLOAD_LIMIT"; + public static final String ACTION_CREATE_FOLDER_NOT_EXIST = "CREATE_FOLDER_NOT_EXIST"; private ServiceHandler mOperationsHandler; private OperationsServiceBinder mOperationsBinder; @@ -714,6 +717,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 postDialogEvent = operationIntent.getBooleanExtra(EXTRA_POST_DIALOG_EVENT, true); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 30b20ac632da..ceef87b391b8 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -86,6 +86,7 @@ import com.nextcloud.utils.extensions.navigateToAllFiles import com.nextcloud.utils.extensions.observeWorker import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFolderPath import com.nextcloud.utils.view.FastScrollUtils +import com.nmc.android.scans.SaveScannedDocumentFragment import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.FilesBinding @@ -1016,6 +1017,22 @@ class FileDisplayActivity : ).execute() } else if (requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK) { exitSelectionMode() + } else if (requestCode == REQUEST_CODE__SCAN_DOCUMENT && resultCode == RESULT_OK) { + var remoteFilePath = + data?.getParcelableExtra(SaveScannedDocumentFragment.EXTRA_SCAN_DOC_REMOTE_PATH) + if (remoteFilePath == null) { + remoteFilePath = getCurrentDir() + } + + Log_OC.d(this, "Scan Document save remote path: " + remoteFilePath?.remotePath) + + // NMC-2418 fix + if (remoteFilePath?.remotePath.equals(getCurrentDir()?.remotePath)) { + Log_OC.d(this, "Both current and scan paths are same. Skipping redirection.") + return + } + + fileListFragment?.onItemClicked(remoteFilePath) } else { super.onActivityResult(requestCode, resultCode, data) } @@ -3105,6 +3122,9 @@ class FileDisplayActivity : @JvmField val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA: Int = REQUEST_CODE__LAST_SHARED + 6 + @JvmField + val REQUEST_CODE__SCAN_DOCUMENT: Int = REQUEST_CODE__LAST_SHARED + 9 + protected val DELAY_TO_REQUEST_REFRESH_OPERATION_LATER: Long = DELAY_TO_REQUEST_OPERATIONS_LATER + 350 private val TAG: String = FileDisplayActivity::class.java.getSimpleName() 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 3730796b21ae..c38601a8d321 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 @@ -65,6 +65,8 @@ open class FolderPickerActivity : private var mSyncBroadcastReceiver: SyncBroadcastReceiver? = null private var mSearchOnlyFolders = false + private var mShowOnlyFolder = false + private var mHideEncryptedFolder = false var isDoNotEnterEncryptedFolder = false private set @@ -124,12 +126,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() } } @@ -138,9 +155,12 @@ open class FolderPickerActivity : } private fun setupUIForChooseButton() { - captionText = resources.getText(R.string.folder_picker_choose_caption_text).toString() + captionText = resources.getText(R.string.choose_location).toString() mSearchOnlyFolders = true - isDoNotEnterEncryptedFolder = true + // NMC-3671 fix + // allow entering into e2ee folder while choosing location + isDoNotEnterEncryptedFolder = false + mShowOnlyFolder = true if (this is FilePickerActivity) { return @@ -148,11 +168,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 handleBackPress() { onBackPressedDispatcher.addCallback( this, @@ -352,7 +379,7 @@ open class FolderPickerActivity : } private fun refreshListOfFilesFragment(fromSearch: Boolean) { - listOfFilesFragment?.listDirectory(false, fromSearch) + listOfFilesFragment?.listDirectoryFolder(false, fromSearch, mShowOnlyFolder, mHideEncryptedFolder) } fun browseToRoot() { @@ -498,7 +525,8 @@ open class FolderPickerActivity : private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { if (result.isSuccess) { val fileListFragment = listOfFilesFragment - fileListFragment?.onItemClicked(storageManager.getFileByPath(operation.remotePath)) + // NMC-3702 fix: requires getFileByDecryptedRemotePath instead of getFileByPath + fileListFragment?.onItemClicked(storageManager.getFileByDecryptedRemotePath(operation.remotePath)) } else { try { DisplayUtils.showSnackMessage( @@ -668,6 +696,12 @@ open class FolderPickerActivity : @JvmField val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION") + @JvmField + val EXTRA_SHOW_ONLY_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_SHOW_ONLY_FOLDER") + + @JvmField + val EXTRA_HIDE_ENCRYPTED_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_HIDE_ENCRYPTED_FOLDER") + const val MOVE_OR_COPY = "MOVE_OR_COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" private val TAG = FolderPickerActivity::class.java.simpleName diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index f4945899e9ea..a72876a726a2 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 @@ -66,6 +66,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; private static final String TAG = "ToolbarActivity"; @@ -88,6 +89,7 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mSearchText = findViewById(R.id.search_text); mSwitchAccountButton = findViewById(R.id.switch_account_button); mNotificationButton = findViewById(R.id.notification_button); + mDefaultToolbarDivider = findViewById(R.id.default_toolbar_divider); if (showSortListButtonGroup) { findViewById(R.id.sort_list_button_group).setVisibility(View.VISIBLE); @@ -220,6 +222,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/CommonOCFileListAdapterInterface.kt b/app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt index 314688f7283e..e44e2d1a555c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt @@ -22,7 +22,9 @@ interface CommonOCFileListAdapterInterface { directory: OCFile, storageManager: FileDataStorageManager, onlyOnDevice: Boolean, - mLimitToMimeType: String + mLimitToMimeType: String, + showOnlyFolder: Boolean, + hideEncryptedFolder: Boolean ) fun setHighlightedItem(file: OCFile) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt index 0c84fe6c84e3..8b34ee3754a6 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt @@ -366,7 +366,9 @@ class GalleryAdapter( directory: OCFile, storageManager: FileDataStorageManager, onlyOnDevice: Boolean, - mLimitToMimeType: String + mLimitToMimeType: String, + showOnlyFolder: Boolean, + hideEncryptedFolder: Boolean ) = Unit override fun setHighlightedItem(file: OCFile) = Unit 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 d8471ec1e2c2..1e1adf658ce8 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 @@ -831,7 +831,9 @@ public void swapDirectory( @NonNull OCFile directory, @NonNull FileDataStorageManager updatedStorageManager, boolean onlyOnDevice, - @NonNull String limitToMimeType) { + @NonNull String limitToMimeType, + boolean showOnlyFolder, + boolean hideEncryptedFolder) { this.onlyOnDevice = onlyOnDevice; @@ -851,6 +853,8 @@ public void swapDirectory( adapterDataProvider, onlyOnDevice, limitToMimeType, + showOnlyFolder, + hideEncryptedFolder, preferences, userId, (newList, fileSortOrder) -> diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index 9d1fd774e87f..ce3a87edd562 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -35,6 +35,8 @@ class OCFileListAdapterHelper { dataProvider: OCFileListAdapterDataProvider, onlyOnDevice: Boolean, limitToMimeType: String, + showOnlyFolder: Boolean, + hideEncryptedFolder: Boolean, preferences: AppPreferences, userId: String, onComplete: (List, FileSortOrder) -> Unit @@ -45,6 +47,8 @@ class OCFileListAdapterHelper { dataProvider, onlyOnDevice, limitToMimeType, + showOnlyFolder, + hideEncryptedFolder, preferences, userId ) @@ -59,6 +63,8 @@ class OCFileListAdapterHelper { dataProvider: OCFileListAdapterDataProvider, onlyOnDevice: Boolean, limitToMimeType: String, + showOnlyFolder: Boolean, + hideEncryptedFolder: Boolean, preferences: AppPreferences, userId: String ): Pair, FileSortOrder> { @@ -72,6 +78,11 @@ class OCFileListAdapterHelper { val filtered = ArrayList(rawResult.size) for (file in rawResult) { + // NMC filter condition to show only folder with or without encrypted folders + if (showOnlyFolder && (!file.isFolder && (hideEncryptedFolder || file.isEncrypted))) { + continue + } + if (!showHiddenFiles && file.isHidden) { continue } 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.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt index d67689722a45..16667910cdb0 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.kt @@ -32,6 +32,7 @@ import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.PermissionUtil import com.owncloud.android.utils.theme.ThemeUtils import com.owncloud.android.utils.theme.ViewThemeUtils +import com.nmc.android.utils.ScanBotSdkUtils @Suppress("LongParameterList") class OCFileListBottomSheetDialog( @@ -65,6 +66,13 @@ class OCFileListBottomSheetDialog( if (!deviceInfo.hasCamera(context)) { binding.menuDirectCameraUpload.visibility = View.GONE + binding.menuScanDocument.visibility = 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.visibility = View.GONE; } createRichWorkspace() @@ -227,6 +235,11 @@ class OCFileListBottomSheetDialog( actions.newPresentation() dismiss() } + + menuScanDocument.setOnClickListener{ + actions.scanDocument() + 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 25a1d1a0b192..97c0d1961a7a 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 @@ -60,6 +60,7 @@ import com.nextcloud.utils.extensions.OCFileExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.fileNameValidator.FileNameValidator; +import com.nmc.android.marketTracking.TrackingScanInterface; import com.nextcloud.utils.view.FastScrollUtils; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -80,6 +81,7 @@ import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.ui.activity.DrawerActivity; +import com.nmc.android.scans.ScanActivity; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; @@ -232,6 +234,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, @@ -239,6 +248,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<>(); @@ -632,6 +643,24 @@ public void createRichWorkspace() { }); } + @Override + public void scanDocument() { + //remote file to store the scans in the selected path + OCFile remoteFile = new OCFile(ROOT_PATH); // default root folder + if (getActivity() != null && ((FileActivity) getActivity()).getCurrentDir() != null){ + remoteFile = ((FileActivity) getActivity()).getCurrentDir(); + } + + //remote path used so that user can directly save at the selected sub folder location + ScanActivity.openScanActivity(getActivity(), remoteFile, FileDisplayActivity.REQUEST_CODE__SCAN_DOCUMENT); + + //track event on Scan Document button click + //implementation and logic will be available in nmc/1925-market_tracking + if (trackingScanInterface != null) { + trackingScanInterface.sendScanEvent(preferences); + } + } + @Override public void onShareIconClick(OCFile file) { if (file.isFolder()) { @@ -1530,6 +1559,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; @@ -1594,7 +1629,9 @@ public void listDirectory(OCFile directory, OCFile file, boolean onlyOnDevice, b directory, storageManager, onlyOnDevice, - mLimitToMimeType); + mLimitToMimeType, + mShowOnlyFolder, + mHideEncryptedFolder); 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 b071a92a9eff..579fb042bb70 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 @@ -993,6 +993,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.kt b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.kt index a116937bebcb..bcb149b3ce26 100644 --- a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.kt +++ b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.kt @@ -22,6 +22,7 @@ object NotificationUtils { const val NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS: String = "NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS" const val NOTIFICATION_CHANNEL_OFFLINE_OPERATIONS: String = "NOTIFICATION_CHANNEL_OFFLINE_OPERATIONS" const val NOTIFICATION_CHANNEL_CONTENT_OBSERVER: String = "NOTIFICATION_CHANNEL_CONTENT_OBSERVER" + const val NOTIFICATION_CHANNEL_IMAGE_SAVE: String = "NOTIFICATION_CHANNEL_IMAGE_SAVE" @JvmStatic fun createUploadNotificationTag(remotePath: String?, localPath: String): String = remotePath + localPath 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 5fb7cfc03894..d3cc9362c261 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 @@ -139,6 +139,38 @@ + + + + + + + + - - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_scanned_document.xml b/app/src/main/res/layout/fragment_edit_scanned_document.xml new file mode 100644 index 000000000000..44363cf2d671 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_scanned_document.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_scan_document.xml b/app/src/main/res/layout/fragment_scan_document.xml new file mode 100644 index 000000000000..c32e31ba4291 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_document.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + 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..504e8422ff82 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_save.xml @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 b44970084204..7b6d995e0a5a 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 63e97d1a9c78..4b1fde700ff0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -133,6 +133,7 @@ Schauen Sie später noch einmal vorbei oder laden Sie neu. Checkbox Wähle einen lokalen Ordner… + Ort wählen Wähle einen entfernten Ordner … Bitte eine Vorlage auswählen und einen Dateinamen eingeben. Wählen Sie, welche Datei behalten werden soll! @@ -164,6 +165,7 @@ Löschen Umbenennen Speichern + Auswählen Senden Teilen Überspringen @@ -1143,6 +1145,7 @@ Herunterladen Video Überlagerungsicon Bitte warten… + Bitte geben Sie unter Apps & Benachrichtigungen in den Einstellungen manuell die Erlaubnis. Überprüfe gespeicherte Anmeldeinformationen Kopiere Datei von privatem Speicher Das Ändern der Erweiterung kann dazu führen, dass diese Datei in einer anderen Anwendung geöffnet wird diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index db1e1d218038..c5134bdaad77 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -39,4 +39,68 @@ @android:color/white #101418 + + + #FFFFFF + @color/grey_30 + @color/grey_30 + #CCCCCC + @color/grey_70 + @color/grey_80 + #2D2D2D + @color/grey_70 + @color/grey_70 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_60 + @color/grey_0 + @color/grey_0 + @color/grey_30 + #FFFFFF + @color/grey_30 + @color/grey_80 + #FFFFFF + + + @color/grey_80 + @color/grey_30 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + @color/grey_80 + + + @color/grey_70 + @color/grey_60 + + + @color/grey_70 + @color/grey_70 + + + #FFFFFF + @color/grey_30 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_60 + @color/grey_0 + #FFFFFF + + + #121212 + @color/grey_0 + @color/grey_80 + @color/grey_80 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 36d7459ecdaf..90c40fb1a4ad 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -89,4 +89,93 @@ #A5A5A5 #F7F9FF + + + #191919 + @color/primary + #191919 + #191919 + @color/grey_30 + @android:color/white + #FFFFFF + @color/grey_0 + #CCCCCC + #77c4ff + #B3FFFFFF + @color/grey_10 + + + #101010 + #F2F2F2 + #E5E5E5 + #B2B2B2 + #666666 + #4C4C4C + #333333 + + + @color/design_snackbar_background_color + @color/white + + + #FFFFFF + #191919 + + + @color/grey_0 + #191919 + @color/primary + #191919 + @color/primary + @color/grey_30 + @color/white + #191919 + + + #FFFFFF + #191919 + #191919 + + + #FFFFFF + #191919 + #FFFFFF + + + @color/primary + #F399C7 + #FFFFFF + @color/grey_30 + @color/grey_10 + @color/grey_0 + + + @color/primary + @color/grey_30 + @color/grey_30 + #CCCCCC + + + #191919 + @color/grey_30 + #191919 + #191919 + #191919 + #191919 + @color/grey_30 + #191919 + #000000 + #191919 + #F6E5EB + #C16F81 + #0D39DF + #0099ff + + + @color/grey_0 + #191919 + @color/grey_0 + @color/grey_30 + #77b6bb + #5077b6bb diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..cc9e25255a10 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,31 @@ + + + 4dp + 16dp + 24dp + 6dp + 18sp + 15sp + 15dp + 56dp + 86dp + 80dp + 11sp + 30dp + 55dp + 258dp + 17sp + 20dp + 160dp + 50dp + 150dp + 55dp + 48dp + 48dp + 24dp + 26dp + 20sp + 145dp + 1dp + 13sp + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_scan_attrs.xml b/app/src/main/res/values/nmc_scan_attrs.xml new file mode 100644 index 000000000000..d69dace876c4 --- /dev/null +++ b/app/src/main/res/values/nmc_scan_attrs.xml @@ -0,0 +1,11 @@ + + + + @string/edit_scan_filter_none + @string/edit_scan_filter_color_enhanced + @string/edit_scan_filter_color_document + @string/edit_scan_filter_grey + @string/edit_scan_filter_b_n_w + @string/edit_scan_filter_pure_binarized + + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_scan_strings.xml b/app/src/main/res/values/nmc_scan_strings.xml new file mode 100644 index 000000000000..cffc108323e3 --- /dev/null +++ b/app/src/main/res/values/nmc_scan_strings.xml @@ -0,0 +1,61 @@ + + + + Scan Document + Do not move + Move closer + Perspective + No document + Background too noisy + Wrong aspect ratio.\nRotate your device. + Poor light + %d of %d + Edit Scan + Crop Scan + Reset Crop + Detect Document + Apply Filter + No Filter + Whiteboard + Photo Filter + Black & White + Document Filter + Grayscale + Automatic + Flash + Save as + Filename + Location + /Root folder + File type + Save without text recognition + Save with text recognition + Textfile (txt) + PDF-Password + Set password + Please select at least one filetype + Please enter a password for the PDF you want to create or disable the function. + You can save the file with or without text recognition. Multiple selection is allowed. + You cannot scan document without camera permission. + Add more document + Crop scanned document + Filter scanned document + Rotate scanned document + Delete scanned document + Edit scan filename + Edit scan location + Ok + Saving will take some time, especially if you have selected several pages and file formats. + PDF + JPG + PNG + PDF (OCR) + Saving files… + Image save notification channel + Shows image save progress + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a48e76517e0a..3220182b78b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1273,6 +1273,7 @@ Link Name Delete Link Settings + Please navigate to App info in settings and give permission manually. Confirm Destination filename Suggest @@ -1305,6 +1306,8 @@ Found no images or videos Error creating file from template No app available for sending the selected files + Choose location + Select All files access Media read-only Don\'t ask diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt index 65db28cfe60a..7d6b8245202f 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt @@ -92,7 +92,9 @@ class OCFileListAdapterHelperTest { onlyOnDevice = false, limitToMimeType = mime, preferences = preferences, - userId = userId + userId = userId, + showOnlyFolder = false, + hideEncryptedFolder = false ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a24bcb59ab0d..eba0a8c70a2c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,6 +75,7 @@ qrcodescannerVersion = "0.1.2.4" reviewKtxVersion = "2.0.2" roomVersion = "2.8.4" screengrabVersion = "2.1.1" +scanbotVersion = "7.1.1" sectionedRecyclerviewVersion = "0.6.1" shotVersion = "6.1.0" slfj = "1.7.36" @@ -100,6 +101,7 @@ document-scanning-android-sdk = { module = "com.github.Hazzatur:Document-Scannin fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtxVersion" } exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterfaceVersion" } material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCoreVersion" } +scanbot-sdk = { module = "io.scanbot:sdk-package-2", version.ref = "scanbotVersion" } webkit = { module = "androidx.webkit:webkit", version.ref = "webkitVersion" } splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splash-screen" } sectioned-recyclerview = { module = "com.github.nextcloud-deps:sectioned-recyclerview", version.ref = "sectionedRecyclerviewVersion" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c0ebf04488ac..77671d6fba8f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,9 @@ dependencyResolutionManagement { } mavenCentral() maven("https://jitpack.io") + // Scanbot SDK maven repos: + maven("https://nexus.scanbot.io/nexus/content/repositories/releases/") + maven("https://nexus.scanbot.io/nexus/content/repositories/snapshots/") } } //includeBuild("../android-common") { @@ -51,4 +54,5 @@ dependencyResolutionManagement { // } //} -include(":app", ":appscan") \ No newline at end of file +// Not required this module in NMC +include(":app") \ No newline at end of file