diff --git a/app/build.gradle b/app/build.gradle index 419d0737be6f..dae93a9cbf81 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,7 +194,7 @@ android { // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure packagingOptions { resources { - excludes += 'META-INF/LICENSE*' + excludes += ['META-INF/LICENSE*', 'META-INF/DEPENDENCIES'] pickFirst 'MANIFEST.MF' // workaround for duplicated manifest on some dependencies } } @@ -296,7 +296,9 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.6.2" implementation 'com.github.albfernandez:juniversalchardet:2.0.3' // need this version for Android <7 compileOnly 'com.google.code.findbugs:annotations:3.0.1u2' - implementation 'commons-io:commons-io:2.16.1' + //Note: Do not change the commons-io:commons-io version from 2.4 to any other + //this is required for ScanBot SDK to work on low end devices ie. 6 & 7 Api levels + implementation 'commons-io:commons-io:2.4' implementation 'org.greenrobot:eventbus:3.3.1' implementation 'com.googlecode.ez-vcard:ez-vcard:0.12.1' implementation 'org.lukhnos:nnio:0.3' @@ -317,10 +319,11 @@ dependencies { implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:$fidoVersion" implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:$fidoVersion" + //NextCloud scan is not required in NMC // document scanner not available on FDroid (generic) due to OpenCV binaries - gplayImplementation project(':appscan') - huaweiImplementation project(':appscan') - qaImplementation project(':appscan') + /* gplayImplementation project(':appscan') + huaweiImplementation project(':appscan') + qaImplementation project(':appscan') */ spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.4' @@ -426,6 +429,14 @@ dependencies { // splash screen dependency ref: https://developer.android.com/develop/ui/views/launch/splash-screen/migrate implementation 'androidx.core:core-splashscreen:1.0.1' + + //scanbot sdk: https://github.com/doo/scanbot-sdk-example-android + implementation "io.scanbot:sdk-package-2:$scanbotSdkVersion" + + //apache pdf-box for encrypting pdf files + implementation(group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.1') { + exclude group: "commons-logging" + } } configurations.configureEach { diff --git a/app/src/androidTest/java/com/nmc/android/ScanActivityMultipleTest.java b/app/src/androidTest/java/com/nmc/android/ScanActivityMultipleTest.java new file mode 100644 index 000000000000..df2630e7c1e2 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ScanActivityMultipleTest.java @@ -0,0 +1,67 @@ +package com.nmc.android; + +import android.Manifest; + +import com.nmc.android.ui.ScanActivity; +import com.owncloud.android.AbstractIT; +import com.owncloud.android.R; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +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 static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.core.IsNot.not; + +@RunWith(AndroidJUnit4.class) +@LargeTest +/** + * Scan test to test the max number of possible scans till device throws exception or unexpected error occurs + */ +public class ScanActivityMultipleTest extends AbstractIT { + + @Rule public ActivityScenarioRule activityRule = new ActivityScenarioRule<>(ScanActivity.class); + + @Rule + public final GrantPermissionRule permissionRule = GrantPermissionRule.grant( + Manifest.permission.CAMERA); + + /** + * variable to define max number of scans to test + */ + private static final int MAX_NUMBER_OF_SCAN = 40; + private int docScanCount = 0; + + @Test + public void runAllScanTests() { + captureAndVerifyDocScan(); + for (int i=0;i activityRule = new ActivityScenarioRule<>(ScanActivity.class); + + @Rule + public final GrantPermissionRule permissionRule = GrantPermissionRule.grant( + Manifest.permission.CAMERA); + + private int docScanCount = 0; + + @Test + /** + * running all test in one test will create a flow from scanning to saving the scans + */ + public void runAllScanTests() { + verifyIfToolbarHidden(); + verifyIfScanFragmentReplaced(); + verifyToggleAutomatic(); + verifyToggleFlash(); + captureAndVerifyDocScan(); + verifyScanMoreDocument(); + verifyApplyFilter(); + verifyRotateDocument(); + verifyImageCrop(); + verifyImageDeletion(); + verifySaveScannedDocs(); + verifyPasswordSwitch(); + verifyPdfPasswordSwitchToggle(); + } + + public void verifyIfToolbarHidden() { + onView(withId(R.id.toolbar)).check(matches(not(isDisplayed()))); + } + + + public void verifyIfScanFragmentReplaced() { + onView(withId(R.id.scan_doc_btn_automatic)).check(matches(isDisplayed())); + onView(withId(R.id.scan_doc_btn_flash)).check(matches(isDisplayed())); + onView(withId(R.id.scan_doc_btn_cancel)).check(matches(isDisplayed())); + onView(withId(R.id.shutterButton)).check(matches(isDisplayed())); + } + + + public void verifyToggleAutomatic() { + onView(withId(R.id.scan_doc_btn_automatic)).perform(click()); + onView(withId(R.id.scan_doc_btn_automatic)).check(matches(hasTextColor(R.color.grey_60))); + + onView(withId(R.id.scan_doc_btn_automatic)).perform(click()); + onView(withId(R.id.scan_doc_btn_automatic)).check(matches(hasTextColor(R.color.primary))); + } + + + public void verifyToggleFlash() { + onView(withId(R.id.scan_doc_btn_flash)).perform(click()); + onView(withId(R.id.scan_doc_btn_flash)).check(matches(hasTextColor(R.color.primary))); + + onView(withId(R.id.scan_doc_btn_flash)).perform(click()); + onView(withId(R.id.scan_doc_btn_flash)).check(matches(hasTextColor(R.color.grey_60))); + } + + + public void captureAndVerifyDocScan() { + onView(withId(R.id.shutterButton)).perform(click()); + shortSleep(); + shortSleep(); + shortSleep(); + docScanCount++; + assertEquals(docScanCount, ScanActivity.originalScannedImages.size()); + } + + public void verifyScanMoreDocument() { + onView(withId(R.id.scanMoreButton)).perform(click()); + captureAndVerifyDocScan(); + } + + public void verifyApplyFilter() { + onView(withId(R.id.filterDocButton)).perform(click()); + + onView(withText(R.string.edit_scan_filter_dialog_title)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); + + onView(withText(R.string.edit_scan_filter_b_n_w)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()); + + shortSleep(); + shortSleep(); + shortSleep(); + } + + + public void verifyRotateDocument() { + onView(withId(R.id.rotateDocButton)).perform(click()); + } + + + public void verifyImageCrop() { + onView(withId(R.id.cropDocButton)).perform(click()); + + onView(withId(R.id.crop_polygon_view)).check(matches(isDisplayed())); + onView(withId(R.id.crop_btn_reset_borders)).check(matches(isDisplayed())); + + onView(withId(R.id.action_save)).perform(click()); + } + + + public void verifyImageDeletion() { + onView(withId(R.id.deleteDocButton)).perform(click()); + docScanCount--; + assertEquals(docScanCount, ScanActivity.originalScannedImages.size()); + } + + + public void verifySaveScannedDocs() { + onView(withId(R.id.action_save)).perform(click()); + + onView(withId(R.id.scan_save_filename_input)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_location_input)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_nested_scroll_view)).perform(swipeUp()); + + onView(withId(R.id.scan_save_without_txt_recognition_pdf_checkbox)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_without_txt_recognition_png_checkbox)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_without_txt_recognition_jpg_checkbox)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_with_txt_recognition_pdf_checkbox)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_with_txt_recognition_txt_checkbox)).check(matches(isDisplayed())); + + onView(withId(R.id.scan_save_without_txt_recognition_pdf_checkbox)).check(matches(not(isChecked()))); + onView(withId(R.id.scan_save_without_txt_recognition_png_checkbox)).check(matches(not(isChecked()))); + onView(withId(R.id.scan_save_without_txt_recognition_jpg_checkbox)).check(matches(not(isChecked()))); + onView(withId(R.id.scan_save_with_txt_recognition_pdf_checkbox)).check(matches(isChecked())); + onView(withId(R.id.scan_save_with_txt_recognition_txt_checkbox)).check(matches(not(isChecked()))); + + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(isDisplayed())); + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(isEnabled())); + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(not(isChecked()))); + onView(withId(R.id.scan_save_pdf_password_text_input)).check(matches(not(isDisplayed()))); + + onView(withId(R.id.save_scan_btn_cancel)).check(matches(isDisplayed())); + onView(withId(R.id.save_scan_btn_save)).check(matches(isDisplayed())); + } + + + public void verifyPasswordSwitch() { + onView(withId(R.id.scan_save_with_txt_recognition_pdf_checkbox)).perform(click()); + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(not(isEnabled()))); + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(not(isChecked()))); + + onView(withId(R.id.scan_save_without_txt_recognition_pdf_checkbox)).perform(click()); + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(isEnabled())); + onView(withId(R.id.scan_save_pdf_password_switch)).check(matches(not(isChecked()))); + + } + + + public void verifyPdfPasswordSwitchToggle() { + onView(withId(R.id.scan_save_pdf_password_switch)).perform(click()); + onView(withId(R.id.scan_save_pdf_password_text_input)).check(matches(isDisplayed())); + + onView(withId(R.id.scan_save_pdf_password_switch)).perform(click()); + onView(withId(R.id.scan_save_pdf_password_text_input)).check(matches(not(isDisplayed()))); + } + +} diff --git a/app/src/androidTest/java/com/nmc/android/ScanbotIT.kt b/app/src/androidTest/java/com/nmc/android/ScanbotIT.kt new file mode 100644 index 000000000000..cf0c8d2f1e0b --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ScanbotIT.kt @@ -0,0 +1,93 @@ +package com.nmc.android + +import android.content.Intent +import android.os.Looper +import androidx.activity.result.contract.ActivityResultContract +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.documentscan.AppScanOptionalFeature +import com.nextcloud.utils.EditorUtils +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions +import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +class ScanbotIT : AbstractIT() { + + @Mock + private lateinit var actions: OCFileListBottomSheetActions + + @get:Rule + var activityRule = IntentsTestRule(FileDisplayActivity::class.java, true, false) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun validateScanButton() { + //Looper to avoid android.util.AndroidRuntimeException: Animators may only be run on Looper threads + //during running test + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val intent = Intent(targetContext, FileDisplayActivity::class.java) + val fda = activityRule.launchActivity(intent) + val info = DeviceInfo() + val ocFile = OCFile("/test.md") + val appScanOptionalFeature: AppScanOptionalFeature = object : AppScanOptionalFeature() { + override fun getScanContract(): ActivityResultContract { + throw UnsupportedOperationException("Document scan is not available") + } + } + + val editorUtils = EditorUtils(ArbitraryDataProviderImpl(targetContext)) + val sut = OCFileListBottomSheetDialog( + fda, + actions, + info, + user, + ocFile, + fda.themeUtils, + activityRule.activity.viewThemeUtils, + editorUtils, + appScanOptionalFeature + ) + + fda.runOnUiThread { sut.show() } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + shortSleep() + + sut.behavior.state = BottomSheetBehavior.STATE_EXPANDED + + //validate nmc scan button visibility & clickable + onView(withId(R.id.menu_scan_document)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.menu_scan_document)).check(matches(isClickable())) + + //validate nc scan button hidden + onView(withId(R.id.menu_scan_doc_upload)).check(matches(not(isDisplayed()))) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + shortSleep() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java index c7e64729eb58..53839b7bb6f1 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java @@ -386,6 +386,11 @@ public void showTemplate(Creator creator, String headline) { public void createRichWorkspace() { } + + @Override + public void scanDocument() { + + } }; DeviceInfo info = new DeviceInfo(); diff --git a/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt b/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt index 8d7728ab7676..b9ded1ed7ddf 100644 --- a/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt +++ b/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt @@ -7,7 +7,6 @@ */ package com.nextcloud.client.di -import com.nextcloud.appscan.ScanPageContract import com.nextcloud.client.documentscan.AppScanOptionalFeature import dagger.Module import dagger.Provides @@ -18,8 +17,6 @@ internal class VariantModule { @Provides @Reusable fun scanOptionalFeature(): AppScanOptionalFeature { - return object : AppScanOptionalFeature() { - override fun getScanContract() = ScanPageContract() - } + return AppScanOptionalFeature.Stub } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8b1b9cc71c6c..dbc4174b25c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -108,6 +108,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/assets/ocr_blobs/deu.traineddata b/app/src/main/assets/ocr_blobs/deu.traineddata new file mode 100644 index 000000000000..97ed7b2b60f2 Binary files /dev/null and b/app/src/main/assets/ocr_blobs/deu.traineddata differ diff --git a/app/src/main/assets/ocr_blobs/eng.traineddata b/app/src/main/assets/ocr_blobs/eng.traineddata new file mode 100644 index 000000000000..bbef4675053b Binary files /dev/null and b/app/src/main/assets/ocr_blobs/eng.traineddata differ diff --git a/app/src/main/assets/ocr_blobs/osd.traineddata b/app/src/main/assets/ocr_blobs/osd.traineddata new file mode 100644 index 000000000000..183644aa5754 Binary files /dev/null and b/app/src/main/assets/ocr_blobs/osd.traineddata differ diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index 60346b2fbb11..ac04ac37a522 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -30,6 +30,10 @@ import com.nextcloud.ui.composeActivity.ComposeActivity; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nmc.android.ui.LauncherActivity; +import com.nmc.android.ui.SaveScannedDocumentFragment; +import com.nmc.android.ui.ScanActivity; +import com.nextcloud.ui.SetStatusDialogFragment; +import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.owncloud.android.MainApp; import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.authentication.DeepLinkLoginActivity; @@ -444,6 +448,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 621052384a8a..bbb63c1efdfc 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 b0087103dadc..ca45c581d459 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -27,6 +27,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences +import com.nmc.android.jobs.ScanDocUploadWorker import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager @@ -93,6 +94,7 @@ class BackgroundJobFactory @Inject constructor( FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters) FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) + ScanDocUploadWorker::class -> createScanDocUploadWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) else -> null // caller falls back to default factory @@ -266,6 +268,15 @@ class BackgroundJobFactory @Inject constructor( ) } + private fun createScanDocUploadWork(context: Context, params: WorkerParameters): ScanDocUploadWorker { + return ScanDocUploadWorker( + context = context, + params = params, + notificationManager, + accountManager + ) + } + private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork { return HealthStatusWork( context, diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 68a7a027ec57..6f172a31ece5 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -155,6 +155,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() @@ -163,4 +170,6 @@ interface BackgroundJobManager { fun cancelAllJobs() fun schedulePeriodicHealthStatus() fun startHealthStatus() + + 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 0e3fd8838678..08a457d55c7a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -21,6 +21,7 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.workDataOf +import com.google.common.util.concurrent.ListenableFuture import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable @@ -31,8 +32,10 @@ import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.OCFile import com.owncloud.android.operations.DownloadType +import com.nmc.android.jobs.ScanDocUploadWorker import java.util.Date import java.util.UUID +import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import kotlin.reflect.KClass @@ -79,6 +82,7 @@ internal class BackgroundJobManagerImpl( const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" + const val JOB_IMMEDIATE_SCAN_DOC_UPLOAD = "immediate_scan_doc_upload" const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" @@ -581,6 +585,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) @@ -626,4 +650,21 @@ internal class BackgroundJobManagerImpl( request ) } + + override fun isWorkScheduled(tag: String): Boolean { + val statuses: ListenableFuture> = workManager.getWorkInfosByTag(tag) + return try { + var running = false + val workInfoList: List = statuses.get() + for (workInfo in workInfoList) { + val state = workInfo.state + running = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED + } + running + } catch (e: ExecutionException) { + false + } catch (e: InterruptedException) { + false + } + } } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index b2517b5fa579..b80a3018b6ea 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -356,6 +356,15 @@ default void onDarkThemeModeChanged(DarkMode mode) { void setPowerCheckDisabled(boolean value); + /** + * Saves the previously selected storage path to save scanned document + * default value will be Scan folder which will be automatically created first time + * @param path of the folder previously selected + */ + void setUploadScansLastPath(String path); + + String getUploadScansLastPath(); + void increasePinWrongAttempts(); void resetPinWrongAttempts(); diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 4531fd20acf4..e4ee21abdb18 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.ui.ScanActivity; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; import com.nextcloud.client.jobs.LogEntry; @@ -79,6 +80,7 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__AUTO_UPLOAD_SPLIT_OUT = "autoUploadEntriesSplitOut"; private static final String PREF__AUTO_UPLOAD_INIT = "autoUploadInit"; private static final String PREF__FOLDER_SORT_ORDER = "folder_sort_order"; + private static final String PREF__UPLOAD_SCANS_LAST_PATH = "upload_scans_last_path"; private static final String PREF__FOLDER_LAYOUT = "folder_layout"; private static final String PREF__LOCK_TIMESTAMP = "lock_timestamp"; @@ -694,6 +696,16 @@ public void setPowerCheckDisabled(boolean value) { preferences.edit().putBoolean(PREF__POWER_CHECK_DISABLED, value).apply(); } + @Override + public void setUploadScansLastPath(String path) { + preferences.edit().putString(PREF__UPLOAD_SCANS_LAST_PATH, path).apply(); + } + + @Override + public String getUploadScansLastPath() { + return preferences.getString(PREF__UPLOAD_SCANS_LAST_PATH, ScanActivity.DEFAULT_UPLOAD_SCAN_PATH); + } + public void increasePinWrongAttempts() { int count = preferences.getInt(PREF__PIN_BRUTE_FORCE_COUNT, 0); preferences.edit().putInt(PREF__PIN_BRUTE_FORCE_COUNT, count + 1).apply(); diff --git a/app/src/main/java/com/nmc/android/adapters/ViewPagerFragmentAdapter.java b/app/src/main/java/com/nmc/android/adapters/ViewPagerFragmentAdapter.java new file mode 100644 index 000000000000..67aab09585e5 --- /dev/null +++ b/app/src/main/java/com/nmc/android/adapters/ViewPagerFragmentAdapter.java @@ -0,0 +1,43 @@ +package com.nmc.android.adapters; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +public class ViewPagerFragmentAdapter extends FragmentStateAdapter { + private final List fragmentList = new ArrayList<>(); + + public ViewPagerFragmentAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + public ViewPagerFragmentAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return fragmentList.get(position); + } + + @Override + public int getItemCount() { + return fragmentList.size(); + } + + public void addFragment(Fragment fragment) { + fragmentList.add(fragment); + } + + public Fragment getFragment(int position){ + if (fragmentList.size() > 0 && position>=0 && fragmentList.size()-1 >= position){ + return fragmentList.get(position); + } + return null; + } +} diff --git a/app/src/main/java/com/nmc/android/interfaces/OnDocScanListener.java b/app/src/main/java/com/nmc/android/interfaces/OnDocScanListener.java new file mode 100644 index 000000000000..f60ba13ae0eb --- /dev/null +++ b/app/src/main/java/com/nmc/android/interfaces/OnDocScanListener.java @@ -0,0 +1,19 @@ +package com.nmc.android.interfaces; + +import android.graphics.Bitmap; + +import java.io.File; +import java.util.List; + +public interface OnDocScanListener { + void addScannedDoc(Bitmap file); + + List getScannedDocs(); + + boolean removedScannedDoc(Bitmap file, int index); + + //isFilterApplied will tell whether the filter is applied to the image or not + Bitmap replaceScannedDoc(int index, Bitmap newFile, boolean isFilterApplied); + + void replaceFilterIndex(int index, int filterIndex); +} diff --git a/app/src/main/java/com/nmc/android/interfaces/OnFragmentChangeListener.java b/app/src/main/java/com/nmc/android/interfaces/OnFragmentChangeListener.java new file mode 100644 index 000000000000..98955c0fb89e --- /dev/null +++ b/app/src/main/java/com/nmc/android/interfaces/OnFragmentChangeListener.java @@ -0,0 +1,7 @@ +package com.nmc.android.interfaces; + +import androidx.fragment.app.Fragment; + +public interface OnFragmentChangeListener { + void onReplaceFragment(Fragment fragment, String tag, boolean addToBackStack); +} 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..53dc193c2e1d --- /dev/null +++ b/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt @@ -0,0 +1,273 @@ +package com.nmc.android.jobs + +import android.app.NotificationManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.text.TextUtils +import androidx.core.app.NotificationCompat +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nmc.android.ui.SaveScannedDocumentFragment +import com.nmc.android.ui.ScanActivity +import com.nmc.android.utils.FileUtils +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.StringUtils +import io.scanbot.ocr.model.OcrPage +import io.scanbot.pdf.model.PageSize +import io.scanbot.pdf.model.PdfConfig +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.ocr.OpticalCharacterRecognizer +import io.scanbot.sdk.ocr.process.OcrResult +import io.scanbot.sdk.persistence.Page +import io.scanbot.sdk.persistence.PageFileStorage +import io.scanbot.sdk.process.PDFRenderer +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.encryption.AccessPermission +import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.security.SecureRandom + +class ScanDocUploadWorker( + private val context: Context, + params: WorkerParameters, + private val notificationManager: NotificationManager, + private val accountManager: UserAccountManager +) : Worker(context, params) { + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var pdfRenderer: PDFRenderer + private lateinit var pageFileStorage: PageFileStorage + private lateinit var opticalCharacterRecognizer: OpticalCharacterRecognizer + private val savedFiles = mutableListOf() + + companion object { + const val TAG = "ScanDocUploadWorkerJob" + const val DATA_REMOTE_PATH = "data_remote_path" + const val DATA_SCAN_FILE_TYPES = "data_scan_file_types" + const val DATA_SCAN_PDF_PWD = "data_scan_pdf_pwd" + const val DATA_DOC_FILE_NAME = "data_doc_file_name" + const val IMAGE_COMPRESSION_PERCENTAGE = 85 + } + + override fun doWork(): Result { + initScanBotSDK() + val remoteFolderPath = inputData.getString(DATA_REMOTE_PATH) + val scanDocFileTypes = inputData.getString(DATA_SCAN_FILE_TYPES) + val scanDocPdfPwd = inputData.getString(DATA_SCAN_PDF_PWD) + val docFileName = inputData.getString(DATA_DOC_FILE_NAME) + + val fileTypes = StringUtils.convertStringToList(scanDocFileTypes) + val bitmapList: List = ScanActivity.filteredImages + + val randomId = SecureRandom() + val pushNotificationId = randomId.nextInt() + showNotification(pushNotificationId) + + for (type in fileTypes) { + when (type) { + SaveScannedDocumentFragment.SAVE_TYPE_JPG -> { + saveJPGImageFiles(docFileName, bitmapList) + } + + SaveScannedDocumentFragment.SAVE_TYPE_PNG -> { + savePNGImageFiles(docFileName, bitmapList) + } + + SaveScannedDocumentFragment.SAVE_TYPE_PDF -> { + saveNonOCRPDFFile(docFileName, bitmapList, scanDocPdfPwd) + } + + SaveScannedDocumentFragment.SAVE_TYPE_PDF_OCR -> { + savePDFWithOCR(docFileName, bitmapList, scanDocPdfPwd) + } + + SaveScannedDocumentFragment.SAVE_TYPE_TXT -> { + saveTextFile(docFileName, bitmapList) + } + } + } + notificationManager.cancel(pushNotificationId) + + uploadScannedDocs(remoteFolderPath) + + return Result.success() + } + + private fun showNotification(pushNotificationId: Int) { + val notificationBuilder = + NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_IMAGE_SAVE) + .setSmallIcon(R.drawable.notification_icon) + .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon)) + .setColor(context.resources.getColor(R.color.primary, null)) + .setContentTitle(context.resources.getString(R.string.app_name)) + .setContentText(context.resources.getString(R.string.foreground_service_save)) + .setAutoCancel(false) + + notificationManager.notify(pushNotificationId, notificationBuilder.build()) + } + + private fun initScanBotSDK() { + scanbotSDK = ScanbotSDK(context) + pdfRenderer = scanbotSDK.createPdfRenderer() + pageFileStorage = scanbotSDK.createPageFileStorage() + opticalCharacterRecognizer = scanbotSDK.createOcrRecognizer() + } + + private fun saveJPGImageFiles(fileName: String?, bitmapList: List) { + for (i in bitmapList.indices) { + var newFileName = fileName + val bitmap = bitmapList[i] + if (i > 0) { + newFileName += "($i)" + } + + val jpgFile = FileUtils.saveJpgImage(context, bitmap, newFileName, IMAGE_COMPRESSION_PERCENTAGE) + savedFiles.add(jpgFile.path) + } + } + + private fun savePNGImageFiles(fileName: String?, bitmapList: List) { + for (i in bitmapList.indices) { + var newFileName = fileName + val bitmap = bitmapList[i] + if (i > 0) { + newFileName += "($i)" + } + + val pngFile = FileUtils.savePngImage(context, bitmap, newFileName, IMAGE_COMPRESSION_PERCENTAGE) + savedFiles.add(pngFile.path) + } + } + + private fun saveNonOCRPDFFile(fileName: String?, bitmapList: List, pdfPassword: String?) { + + val pageList = getScannedPages(bitmapList) + val pdfFile: File? = + pdfRenderer.renderDocumentFromPages(pageList, PdfConfig.defaultConfig().copy(pageSize = PageSize.A4)) + if (pdfFile != null) { + val renamedFile = File(pdfFile.parent + OCFile.PATH_SEPARATOR + fileName + ".pdf") + if (pdfFile.renameTo(renamedFile)) { + Log_OC.d(TAG, "File successfully renamed") + } + savePdfFile(pdfPassword, renamedFile) + } + } + + /** + * save pdf file if pdf password is set else add it to list + */ + private fun savePdfFile(pdfPassword: String?, renamedFile: File) { + if (!TextUtils.isEmpty(pdfPassword)) { + pdfWithPassword(renamedFile, pdfPassword) + } else { + savedFiles.add(renamedFile.path) + } + } + + private fun pdfWithPassword(pdfFile: File, pdfPassword: String?) { + try { + val document: PDDocument = PDDocument.load(pdfFile) + //Creating access permission object + val ap = AccessPermission() + //Creating StandardProtectionPolicy object + val spp = StandardProtectionPolicy(pdfPassword, pdfPassword, ap) + //Setting the length of the encryption key + spp.encryptionKeyLength = 128 + //Setting the access permissions + spp.permissions = ap + //Protecting the document + document.protect(spp) + + //save the encrypted pdf file + val os = FileOutputStream(pdfFile) + document.save(os) + + //close the document + document.close() + + //add the file to list + savedFiles.add(pdfFile.path) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } + } + + private fun getScannedPages(bitmapList: List): List { + val pageList: MutableList = ArrayList() + for (bitmap in bitmapList) { + val page = Page(pageFileStorage.add(bitmap), listOf(), DetectionStatus.OK) + pageList.add(page) + } + return pageList + } + + private fun savePDFWithOCR(fileName: String?, bitmapList: List, pdfPassword: String?) { + val ocrResult: OcrResult = + opticalCharacterRecognizer.recognizeTextWithPdfFromPages( + getScannedPages(bitmapList), + PdfConfig.defaultConfig().copy(pageSize = PageSize.A4) + ) + val ocrPageList: List = ocrResult.ocrPages + if (ocrPageList.isNotEmpty()) { + val ocrText = ocrResult.recognizedText + } + val ocrPDFFile = ocrResult.sandwichedPdfDocumentFile + if (ocrPDFFile != null) { + val renamedFile = File(ocrPDFFile.parent + OCFile.PATH_SEPARATOR + fileName + "_OCR.pdf") + if (ocrPDFFile.renameTo(renamedFile)) { + Log_OC.d(TAG, "OCR File successfully renamed") + } + savePdfFile(pdfPassword, renamedFile) + } + } + + private fun saveTextFile(fileName: String?, bitmapList: List) { + for (i in bitmapList.indices) { + var newFileName = fileName + val bitmap = bitmapList[i] + if (i > 0) { + newFileName += "($i)" + } + val page = Page(pageFileStorage.add(bitmap), emptyList(), DetectionStatus.OK) + val pageList: MutableList = ArrayList() + pageList.add(page) + val ocrResult: OcrResult = opticalCharacterRecognizer.recognizeTextFromPages(pageList) + val ocrPageList: List = ocrResult.ocrPages + if (ocrPageList.isNotEmpty()) { + val ocrText = ocrResult.recognizedText + val txtFile = FileUtils.writeTextToFile(context, ocrText, newFileName) + savedFiles.add(txtFile.path) + } + } + } + + private fun uploadScannedDocs(remotePathBase: String?) { + val remotePaths = savedFiles.map { + remotePathBase + File(it).name + }.toTypedArray() + + FileUploadHelper.instance().uploadNewFiles( + accountManager.user, + savedFiles.toTypedArray(), + remotePaths, + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + false, // do not create parent folder if not existent + UploadFileOperation.CREATED_BY_USER, + false, + false, + NameCollisionPolicy.RENAME + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/marketTracking/TrackingScanInterface.kt b/app/src/main/java/com/nmc/android/marketTracking/TrackingScanInterface.kt new file mode 100644 index 000000000000..3a517a29f458 --- /dev/null +++ b/app/src/main/java/com/nmc/android/marketTracking/TrackingScanInterface.kt @@ -0,0 +1,14 @@ +package com.nmc.android.marketTracking + +import com.nextcloud.client.preferences.AppPreferences + +/** + * interface to track the scanning events from nmc/1867-scanbot branch + * for implementation look nmc/1925-market_tracking branch + * this class will have the declaration for it since it has the tracking SDK's in place + * since we don't have scanning functionality in this branch so to handle the event we have used interface + */ +interface TrackingScanInterface { + + fun sendScanEvent(appPreferences: AppPreferences) +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt b/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt new file mode 100644 index 000000000000..2a00b6c66fd2 --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt @@ -0,0 +1,329 @@ +package com.nmc.android.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ActivityInfo +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.PointF +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.util.Pair +import android.view.* +import androidx.fragment.app.Fragment +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.nmc.android.utils.ScanBotSdkUtils +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentCropScanBinding +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.core.contourdetector.ContourDetector +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.core.contourdetector.Line2D +import io.scanbot.sdk.process.CropOperation +import io.scanbot.sdk.process.ImageProcessor +import java.util.concurrent.Executors +import kotlin.math.absoluteValue + +class CropScannedDocumentFragment : Fragment() { + private lateinit var binding: FragmentCropScanBinding + private lateinit var onFragmentChangeListener: OnFragmentChangeListener + private lateinit var onDocScanListener: OnDocScanListener + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var imageProcessor: ImageProcessor + private lateinit var contourDetector: ContourDetector + + private var scannedDocIndex: Int = -1 + private lateinit var originalBitmap: Bitmap + private var rotationDegrees = 0 + private var polygonPoints: List? = null + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.getInt(ARG_SCANNED_DOC_INDEX)?.let { + scannedDocIndex = it + } + // Fragment locked in portrait screen orientation + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + override fun onAttach(context: Context) { + super.onAttach(context) + run { + try { + onFragmentChangeListener = context as OnFragmentChangeListener + onDocScanListener = context as OnDocScanListener + } catch (ignored: Exception) { + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(true) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(true) + (requireActivity() as ScanActivity).updateActionBarTitleAndHomeButtonByString(resources.getString(R.string.title_crop_scan)) + } + setHasOptionsMenu(true) + binding = FragmentCropScanBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + contourDetector = scanbotSDK.createContourDetector() + imageProcessor = scanbotSDK.imageProcessor() + + detectDocument() + binding.cropBtnResetBorders.setOnClickListener { + onClickListener(it) + } + addExtraMarginForSwipeGesture() + } + + /** + * method to add extra margins for gestured devices + * where user has to swipe left or right to go back from current screen + * this swipe gestures create issue with existing crop gestures + * to avoid that we have added extra margins on left and right for devices + * greater than API level 9+ (Pie) + */ + private fun addExtraMarginForSwipeGesture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (binding.cropPolygonView.layoutParams is ViewGroup.MarginLayoutParams) { + (binding.cropPolygonView.layoutParams as ViewGroup.MarginLayoutParams).setMargins( + resources.getDimensionPixelOffset(R.dimen.standard_margin), 0, + resources.getDimensionPixelOffset(R.dimen.standard_margin), 0 + ) + binding.cropPolygonView.requestLayout() + } + } + } + + private fun onCropDragListener() { + polygonPoints?.let { points -> + var previous = points + binding.cropPolygonView.setEditPolygonDragStateListener { dragging -> + if (dragging) { + previous = ArrayList(binding.cropPolygonView.polygon.map { PointF(it.x, it.y) }) + } else { + if (!isBigEnough(binding.cropPolygonView.polygon)) { + binding.cropPolygonView.polygon = previous + } + } + } + } + } + + private fun onClickListener(view: View) { + when (view.id) { + R.id.crop_btn_reset_borders -> { + if (binding.cropBtnResetBorders.tag.equals(resources.getString(R.string.crop_btn_reset_crop_text))) { + updateButtonText(resources.getString(R.string.crop_btn_detect_doc_text)) + resetCrop() + } else if (binding.cropBtnResetBorders.tag.equals(resources.getString(R.string.crop_btn_detect_doc_text))) { + updateButtonText(resources.getString(R.string.crop_btn_reset_crop_text)) + detectDocument() + } + } + } + } + + private fun updateButtonText(label: String) { + binding.cropBtnResetBorders.tag = label + binding.cropBtnResetBorders.text = label + } + + private fun resetCrop() { + polygonPoints = getResetPolygons() + binding.cropPolygonView.polygon = getResetPolygons() + onCropDragListener() + } + + private fun getResetPolygons(): List { + val polygonList = mutableListOf() + val pointF = PointF(0.0f, 0.0f) + val pointF1 = PointF(1.0f, 0.0f) + val pointF2 = PointF(1.0f, 1.0f) + val pointF3 = PointF(0.0f, 1.0f) + polygonList.add(pointF) + polygonList.add(pointF1) + polygonList.add(pointF2) + polygonList.add(pointF3) + return polygonList + } + + private fun detectDocument() { + InitImageViewTask().executeOnExecutor(Executors.newSingleThreadExecutor()) + } + + @SuppressLint("StaticFieldLeak") + internal inner class InitImageViewTask : AsyncTask() { + private var previewBitmap: Bitmap? = null + + override fun doInBackground(vararg params: Void?): InitImageResult { + originalBitmap = onDocScanListener.scannedDocs[scannedDocIndex] + previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap) + + val result = contourDetector.detect(originalBitmap) + return when (result?.status) { + DetectionStatus.OK, + DetectionStatus.OK_BUT_BAD_ANGLES, + DetectionStatus.OK_BUT_TOO_SMALL, + DetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + val linesPair = Pair(result.horizontalLines, result.verticalLines) + val polygon = result.polygonF + + InitImageResult(linesPair, polygon) + } + + else -> InitImageResult(Pair(listOf(), listOf()), listOf()) + } + } + + override fun onPostExecute(initImageResult: InitImageResult) { + binding.cropPolygonView.setImageBitmap(previewBitmap) + binding.magnifier.setupMagnifier(binding.cropPolygonView) + + // set detected polygon and lines into binding.cropPolygonView + polygonPoints = initImageResult.polygon + binding.cropPolygonView.polygon = initImageResult.polygon + binding.cropPolygonView.setLines(initImageResult.linesPair.first, initImageResult.linesPair.second) + + if (initImageResult.polygon.isEmpty()) { + resetCrop() + } else { + onCropDragListener() + } + } + } + + internal inner class InitImageResult(val linesPair: Pair, List>, val polygon: List) + + private fun crop() { + // crop & warp image by selected polygon (editPolygonView.getPolygon()) + val operations = listOf(CropOperation(binding.cropPolygonView.polygon)) + + var documentImage = imageProcessor.processBitmap(originalBitmap, operations, false) + documentImage?.let { + if (rotationDegrees > 0) { + // rotate the final cropped image result based on current rotation value: + val matrix = Matrix() + matrix.postRotate(rotationDegrees.toFloat()) + documentImage = Bitmap.createBitmap(it, 0, 0, it.width, it.height, matrix, true) + } + onDocScanListener.replaceScannedDoc(scannedDocIndex, documentImage, false) + + onFragmentChangeListener.onReplaceFragment( + EditScannedDocumentFragment.newInstance(scannedDocIndex), ScanActivity + .FRAGMENT_EDIT_SCAN_TAG, false + ) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.edit_scan, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_save -> { + crop() + } + } + return super.onOptionsItemSelected(item) + } + + fun getScannedDocIndex(): Int { + return scannedDocIndex + } + + private fun isBigEnough(polygon: List): Boolean { + if (polygon.isEmpty()) { + return true + } + + /* + We receive the array of 4 Polygons when user start dragging the borders to crop the document + 1. polygon[0].x to polygon[3].x --> When user drag from left to right or right to left + 2. polygon[0].y to polygon[3].y --> When user drag from top to bottom or bottom to top + + Now to find the minimum difference we need to compare X and Y polygons. Here we have 2 cases: + 1. For Y polygon: + 1.1. When user dragging from Top to Bottom --> In this case Y will have same value in 0 & 1 index + i.e. polygon[0].y & polygon[1].y + + 1.2. When user dragging from Bottom to Top --> In this case Y will have same value in 2 & 3 index + i.e. polygon[2].y & polygon[3].y + + 2. For X polygon: + 2.1. When user dragging from Left to Right --> In this case X will have same value in 0 & 3 index + i.e. polygon[0].x & polygon[3].x + + 2.2. When user dragging from Right to Left --> In this case X will have same value in 1 & 2 index + i.e. polygon[1].x & polygon[2].x + + + Now to avoid user cropping the whole document we need to have minimum cropping point. To do that + we need to check the difference between the polygon for X and Y like: + 1. For Y: check the difference between polygon[0].y - polygon[2].y and so on + 2. For X: check the difference between polygon[0].x - polygon[1].x and so on + + */ + + if ((polygon[0].y - polygon[2].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[0].y - polygon[3].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[1].y - polygon[2].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[1].y - polygon[3].y).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[0].x - polygon[1].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[0].x - polygon[2].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[3].x - polygon[1].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + if ((polygon[3].x - polygon[2].x).absoluteValue < MINIMUM_CROP_REQUIRED) { + return false + } + + return true + } + + companion object { + private const val ARG_SCANNED_DOC_INDEX = "scanned_doc_index" + + //variable used to avoid cropping the whole document + private const val MINIMUM_CROP_REQUIRED = 0.1 + + @JvmStatic + fun newInstance(index: Int): CropScannedDocumentFragment { + val args = Bundle() + args.putInt(ARG_SCANNED_DOC_INDEX, index) + val fragment = CropScannedDocumentFragment() + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/ui/EditScannedDocumentFragment.java b/app/src/main/java/com/nmc/android/ui/EditScannedDocumentFragment.java new file mode 100644 index 000000000000..c6f9eb8b99e5 --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/EditScannedDocumentFragment.java @@ -0,0 +1,223 @@ +package com.nmc.android.ui; + +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 com.nmc.android.adapters.ViewPagerFragmentAdapter; +import com.nmc.android.interfaces.OnDocScanListener; +import com.nmc.android.interfaces.OnFragmentChangeListener; +import com.owncloud.android.R; +import com.owncloud.android.databinding.FragmentEditScannedDocumentBinding; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.widget.ViewPager2; + +public class EditScannedDocumentFragment extends Fragment implements View.OnClickListener { + + private static final String ARG_CURRENT_INDEX = "current_index"; + protected static final String TAG = "EditScannedDocumentFragment"; + + public EditScannedDocumentFragment() { + } + + public static EditScannedDocumentFragment newInstance(int currentIndex) { + Bundle args = new Bundle(); + args.putInt(ARG_CURRENT_INDEX, currentIndex); + EditScannedDocumentFragment fragment = new EditScannedDocumentFragment(); + fragment.setArguments(args); + return fragment; + } + + private FragmentEditScannedDocumentBinding binding; + private ViewPagerFragmentAdapter pagerFragmentAdapter; + private OnFragmentChangeListener onFragmentChangeListener; + private OnDocScanListener onDocScanListener; + + private Bitmap selectedScannedDocFile; + private int currentSelectedItemIndex; + private int currentItemIndex; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + currentItemIndex = getArguments().getInt(ARG_CURRENT_INDEX, 0); + } + //Fragment screen orientation normal both portrait and landscape + requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + try { + onFragmentChangeListener = (OnFragmentChangeListener) context; + onDocScanListener = (OnDocScanListener) context; + } catch (Exception ignored) { + + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (requireActivity() instanceof ScanActivity) { + ((ScanActivity) requireActivity()).showHideToolbar(true); + ((ScanActivity) requireActivity()).showHideDefaultToolbarDivider(true); + ((ScanActivity) requireActivity()).updateActionBarTitleAndHomeButtonByString(getResources().getString(R.string.title_edit_scan)); + } + setHasOptionsMenu(true); + binding = FragmentEditScannedDocumentBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + 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); + + } + + private void setUpViewPager() { + pagerFragmentAdapter = new ViewPagerFragmentAdapter(this); + List filesList = onDocScanListener.getScannedDocs(); + if (filesList.size() == 0) { + onScanMore(true); + return; + } + for (int i = 0; i < filesList.size(); i++) { + pagerFragmentAdapter.addFragment(ScanPagerFragment.newInstance(i)); + } + binding.editScannedViewPager.setAdapter(pagerFragmentAdapter); + binding.editScannedViewPager.post(() -> binding.editScannedViewPager.setCurrentItem(currentItemIndex, false)); + binding.editScannedViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + currentSelectedItemIndex = position; + selectedScannedDocFile = filesList.get(position); + updateDocCountText(position, filesList.size()); + } + }); + + if (filesList.size() == 1) { + binding.editScanDocCountLabel.setVisibility(View.INVISIBLE); + } else { + binding.editScanDocCountLabel.setVisibility(View.VISIBLE); + updateDocCountText(currentItemIndex, filesList.size()); + } + } + + private void updateDocCountText(int position, int totalSize) { + binding.editScanDocCountLabel.setText(String.format(getResources().getString(R.string.scanned_doc_count), + position + 1, totalSize)); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.scanMoreButton: + onScanMore(false); + break; + case R.id.cropDocButton: + onFragmentChangeListener.onReplaceFragment(CropScannedDocumentFragment.newInstance(currentSelectedItemIndex), + ScanActivity.FRAGMENT_CROP_SCAN_TAG, false); + break; + case R.id.filterDocButton: + showFilterDialog(); + break; + case R.id.rotateDocButton: + Fragment fragment = pagerFragmentAdapter.getFragment(currentSelectedItemIndex); + if (fragment instanceof ScanPagerFragment) { + ((ScanPagerFragment) fragment).rotate(); + } + break; + case R.id.deleteDocButton: + boolean isRemoved = onDocScanListener.removedScannedDoc(selectedScannedDocFile, currentSelectedItemIndex); + if (isRemoved) { + setUpViewPager(); + } + break; + + } + } + + @Override + public void onConfigurationChanged(@NonNull @NotNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + setUpViewPager(); + } + + /** + * check if fragment has to open on + button click or when all scans removed + * + * @param isNoItem + */ + private void onScanMore(boolean isNoItem) { + onFragmentChangeListener.onReplaceFragment(ScanDocumentFragment.newInstance(isNoItem ? ScanActivity.TAG : TAG), + ScanActivity.FRAGMENT_SCAN_TAG, false); + } + + private void showFilterDialog() { + Fragment fragment = pagerFragmentAdapter.getFragment(currentSelectedItemIndex); + if (fragment instanceof ScanPagerFragment) { + ((ScanPagerFragment) fragment).showApplyFilterDialog(); + } + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.edit_scan, menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_save: + Fragment fragment = pagerFragmentAdapter.getFragment(currentSelectedItemIndex); + if (fragment instanceof ScanPagerFragment) { + //if applying filter is not in process then only show save fragment + if(!((ScanPagerFragment) fragment).isFilterApplyInProgress()){ + saveScannedDocs(); + } + }else { + saveScannedDocs(); + } + break; + } + return super.onOptionsItemSelected(item); + } + + private void saveScannedDocs() { + onFragmentChangeListener.onReplaceFragment(SaveScannedDocumentFragment.newInstance(), + ScanActivity.FRAGMENT_SAVE_SCAN_TAG, false); + } + + @Override + public void onDestroyView() { + binding = null; + super.onDestroyView(); + } +} diff --git a/app/src/main/java/com/nmc/android/ui/SaveScannedDocumentFragment.java b/app/src/main/java/com/nmc/android/ui/SaveScannedDocumentFragment.java new file mode 100644 index 000000000000..b3d6a6f9544f --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/SaveScannedDocumentFragment.java @@ -0,0 +1,363 @@ +package com.nmc.android.ui; + +import android.app.Activity; +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.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.CompoundButton; +import android.widget.ScrollView; + +import com.nextcloud.client.di.Injectable; +import com.nextcloud.client.jobs.BackgroundJobManager; +import com.nextcloud.client.preferences.AppPreferences; +import com.nmc.android.utils.CheckableThemeUtils; +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; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +public class SaveScannedDocumentFragment extends Fragment implements CompoundButton.OnCheckedChangeListener, + Injectable, View.OnClickListener { + + protected static final String TAG = "SaveScannedDocumentFragment"; + private static final int SELECT_LOCATION_REQUEST_CODE = 212; + + public SaveScannedDocumentFragment() { + } + + public static SaveScannedDocumentFragment newInstance() { + Bundle args = new Bundle(); + SaveScannedDocumentFragment fragment = new SaveScannedDocumentFragment(); + fragment.setArguments(args); + return fragment; + } + + private FragmentScanSaveBinding binding; + + public static final String SAVE_TYPE_PDF = "pdf"; + public static final String SAVE_TYPE_PNG = "png"; + public static final String SAVE_TYPE_JPG = "jpg"; + public static final String SAVE_TYPE_PDF_OCR = "pdf_ocr"; + public static final String SAVE_TYPE_TXT = "txt"; + + public static final String EXTRA_SCAN_DOC_REMOTE_PATH = "scan_doc_remote_path"; + + private boolean isFileNameEditable = false; + private String remotePath = "/"; + private OCFile remoteFilePath; + + @Inject BackgroundJobManager backgroundJobManager; + @Inject AppPreferences appPreferences; + + @Override + public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //Fragment screen orientation normal both portrait and landscape + requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + setRetainInstance(true); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (requireActivity() instanceof ScanActivity) { + ((ScanActivity) requireActivity()).showHideToolbar(true); + ((ScanActivity) requireActivity()).showHideDefaultToolbarDivider(true); + ((ScanActivity) requireActivity()).updateActionBarTitleAndHomeButtonByString(getResources().getString(R.string.title_save_as)); + } + binding = FragmentScanSaveBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(); + prepareRemotePath(); + implementCheckListeners(); + implementClickEvent(); + } + + private void 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 void 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() instanceof ScanActivity) { + String remotePath = ((ScanActivity) requireActivity()).getRemotePath(); + //remote path should not be null and should not be root path i.e only / + if (!TextUtils.isEmpty(remotePath) && !remotePath.equals(OCFile.ROOT_PATH)) { + setRemoteFilePath(remotePath); + return; + } + + String lastRemotePath = appPreferences.getUploadScansLastPath(); + //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.equals(OCFile.ROOT_PATH) && !lastRemotePath.equals(ScanActivity.DEFAULT_UPLOAD_SCAN_PATH)) { + setRemoteFilePath(remotePath); + return; + } + } + + setRemoteFilePath(appPreferences.getUploadScansLastPath()); + + } + + protected void setRemoteFilePath(String remotePath) { + remoteFilePath = new OCFile(remotePath); + remoteFilePath.setFolder(); + + updateSaveLocationText(remotePath); + } + + private void initViews() { + binding.scanSaveFilenameInput.setText(FileUtils.scannedFileName()); + CheckableThemeUtils.tintSwitch(binding.scanSavePdfPasswordSwitch); + CheckableThemeUtils.tintCheckbox(binding.scanSaveWithoutTxtRecognitionPdfCheckbox, + binding.scanSaveWithoutTxtRecognitionPngCheckbox, + binding.scanSaveWithoutTxtRecognitionJpgCheckbox, + binding.scanSaveWithTxtRecognitionPdfCheckbox, + binding.scanSaveWithTxtRecognitionTxtCheckbox); + binding.scanSaveWithTxtRecognitionPdfCheckbox.setChecked(true); + binding.scanSavePdfPasswordTextInput.setDefaultHintTextColor(new ColorStateList( + new int[][]{ + new int[]{-android.R.attr.state_focused}, + new int[]{android.R.attr.state_focused}, + }, + new int[]{ + Color.GRAY, + getResources().getColor(R.color.text_color, null) + } + )); + } + + private void 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, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + enableFileNameEditing(); + return true; + } + return false; + }); + } + + private void enableDisablePdfPasswordSwitch() { + binding.scanSavePdfPasswordSwitch.setEnabled(binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked() || binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked()); + if (!binding.scanSaveWithoutTxtRecognitionPdfCheckbox.isChecked() && !binding.scanSaveWithTxtRecognitionPdfCheckbox.isChecked()) { + binding.scanSavePdfPasswordSwitch.setChecked(false); + } + } + + private void showHidePdfPasswordInput(boolean isChecked) { + binding.scanSavePdfPasswordTextInput.setVisibility(isChecked ? View.VISIBLE : 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 void enableFileNameEditing() { + isFileNameEditable = !isFileNameEditable; + binding.scanSaveFilenameInput.setEnabled(isFileNameEditable); + if (isFileNameEditable) { + binding.scanSaveFilenameInputEditBtn.setImageResource(R.drawable.ic_tick); + KeyboardUtils.showSoftKeyboard(requireContext(), binding.scanSaveFilenameInput); + binding.scanSaveFilenameInput.setSelection(binding.scanSaveFilenameInput.getText().toString().trim().length()); + } else { + binding.scanSaveFilenameInputEditBtn.setImageResource(R.drawable.ic_pencil_edit); + KeyboardUtils.hideKeyboardFrom(requireContext(), binding.scanSaveFilenameInput); + } + } + + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.scan_save_filename_input_edit_btn: + enableFileNameEditing(); + break; + case R.id.scan_save_location_edit_btn: + Intent action = new Intent(requireActivity(), FolderPickerActivity.class); + action.putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION); + action.putExtra(FolderPickerActivity.EXTRA_SHOW_ONLY_FOLDER, true); + action.putExtra(FolderPickerActivity.EXTRA_HIDE_ENCRYPTED_FOLDER, false); + startActivityForResult(action, SELECT_LOCATION_REQUEST_CODE); + break; + case R.id.save_scan_btn_cancel: + requireActivity().onBackPressed(); + break; + case R.id.save_scan_btn_save: + saveScannedFiles(); + break; + + } + } + + private void saveScannedFiles() { + String fileName = binding.scanSaveFilenameInput.getText().toString().trim(); + 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; + } + + StringBuilder fileTypesStringBuilder = new 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); + } + String pdfPassword = binding.scanSavePdfPasswordEt.getText().toString().trim(); + if (binding.scanSavePdfPasswordSwitch.isChecked() && TextUtils.isEmpty(pdfPassword)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.save_scan_empty_pdf_password); + return; + } + + showPromptToSave(fileName, fileTypesStringBuilder, pdfPassword); + } + + private void showPromptToSave(String fileName, StringBuilder fileTypesStringBuilder, String pdfPassword) { + try { + AlertDialog alertDialog = new AlertDialog.Builder(requireContext()) + .setMessage(R.string.dialog_save_scan_message) + .setPositiveButton(R.string.dialog_ok, (dialog, which) -> startSaving(fileName, + fileTypesStringBuilder, pdfPassword)) + .create(); + + alertDialog.show(); + } catch (WindowManager.BadTokenException e) { + Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.getMessage()); + } + } + + private void startSaving(String fileName, StringBuilder fileTypesStringBuilder, String pdfPassword) { + //start the save and upload worker + backgroundJobManager.scheduleImmediateScanDocUploadJob( + fileTypesStringBuilder.toString(), + fileName, + remotePath, + pdfPassword); + + //save the selected location to save scans in preference + appPreferences.setUploadScansLastPath(remotePath); + + //send the result back with the selected remote path to open selected remote path + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putParcelable(EXTRA_SCAN_DOC_REMOTE_PATH, remoteFilePath); + intent.putExtras(bundle); + requireActivity().setResult(Activity.RESULT_OK, intent); + requireActivity().finish(); + } + + @Override + public void onDestroyView() { + binding = null; + super.onDestroyView(); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + switch (buttonView.getId()) { + case R.id.scan_save_without_txt_recognition_pdf_checkbox: + case R.id.scan_save_with_txt_recognition_pdf_checkbox: + enableDisablePdfPasswordSwitch(); + break; + case R.id.scan_save_pdf_password_switch: + showHidePdfPasswordInput(isChecked); + break; + } + } + + private void updateSaveLocationText(String path) { + remotePath = path; + if (path.equalsIgnoreCase(OCFile.ROOT_PATH)) { + path = getResources().getString(R.string.scan_save_location_root); + } + binding.scanSaveLocationInput.setText(path); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == SELECT_LOCATION_REQUEST_CODE) { + if (data != null) { + OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER); + if (chosenFolder != null) { + remoteFilePath = chosenFolder; + updateSaveLocationText(chosenFolder.getRemotePath()); + } + } + } + super.onActivityResult(requestCode, resultCode, data); + } + +} diff --git a/app/src/main/java/com/nmc/android/ui/ScanActivity.java b/app/src/main/java/com/nmc/android/ui/ScanActivity.java new file mode 100644 index 000000000000..6ec5023c87ff --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/ScanActivity.java @@ -0,0 +1,272 @@ +package com.nmc.android.ui; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.MenuItem; + +import com.nextcloud.client.preferences.AppPreferences; +import com.nmc.android.interfaces.OnDocScanListener; +import com.nmc.android.interfaces.OnFragmentChangeListener; +import com.owncloud.android.R; +import com.owncloud.android.databinding.ActivityScanBinding; +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.operations.CreateFolderIfNotExistOperation; +import com.owncloud.android.ui.activity.FileActivity; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.res.ResourcesCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import io.scanbot.sdk.ScanbotSDK; + +public class ScanActivity extends FileActivity implements OnFragmentChangeListener, OnDocScanListener { + + protected static final String FRAGMENT_SCAN_TAG = "SCAN_FRAGMENT_TAG"; + protected static final String FRAGMENT_EDIT_SCAN_TAG = "EDIT_SCAN_FRAGMENT_TAG"; + protected static final String FRAGMENT_CROP_SCAN_TAG = "CROP_SCAN_FRAGMENT_TAG"; + protected static final String FRAGMENT_SAVE_SCAN_TAG = "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 + public static final String DEFAULT_UPLOAD_SCAN_PATH = OCFile.ROOT_PATH + "Scans" + OCFile.PATH_SEPARATOR; + + protected static final String TAG = "ScanActivity"; + private static final String EXTRA_REMOTE_PATH = "com.nmc.android.ui.scan_activity.extras.remote_path"; + + private ActivityScanBinding binding; + private ScanbotSDK scanbotSDK; + + public static final List originalScannedImages = new ArrayList<>();//list with original bitmaps + public static final List filteredImages = new ArrayList<>();//list with bitmaps applied filters + public static final List scannedImagesFilterIndex = new ArrayList<>();//list to maintain the state of + // applied filter index when device rotated + + @Inject AppPreferences appPreferences; + + private String remotePath; + //flag to avoid checking folder existence whenever user goes to save fragment + //we will make it true when the operation finishes first time + private boolean isFolderCheckOperationFinished; + + public static void openScanActivity(Context context, String remotePath, int requestCode) { + Intent intent = new Intent(context, ScanActivity.class); + intent.putExtra(EXTRA_REMOTE_PATH, remotePath); + ((AppCompatActivity) context).startActivityForResult(intent, requestCode); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Inflate and set the layout view + binding = ActivityScanBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + remotePath = getIntent().getStringExtra(EXTRA_REMOTE_PATH); + originalScannedImages.clear(); + filteredImages.clear(); + scannedImagesFilterIndex.clear(); + initScanbotSDK(); + setupToolbar(); + setupActionBar(); + } + + private void setupActionBar() { + ActionBar actionBar = getDelegate().getSupportActionBar(); + + if (actionBar != null) { + //required for NMC + actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.bg_default, null))); + actionBar.setDisplayHomeAsUpEnabled(true); + //NMC Customization + viewThemeUtils.files.themeActionBar(this, actionBar, false); + } + } + + public String getRemotePath() { + return remotePath; + } + + @Override + protected void onPostCreate(@Nullable Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + createScanFragment(savedInstanceState); + } + + private void createScanFragment(Bundle savedInstanceState) { + if (savedInstanceState == null) { + ScanDocumentFragment scanDocumentFragment = ScanDocumentFragment.newInstance(TAG); + onReplaceFragment(scanDocumentFragment, FRAGMENT_SCAN_TAG, false); + } else { + getSupportFragmentManager().findFragmentByTag(FRAGMENT_SCAN_TAG); + } + } + + @Override + public void onReplaceFragment(Fragment fragment, String tag, boolean addToBackStack) { + //only during replacing save scan fragment + if (tag.equalsIgnoreCase(FRAGMENT_SAVE_SCAN_TAG)) { + checkAndCreateFolderIfRequired(); + } + + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.scan_frame_container, fragment, tag); + if (addToBackStack) { + transaction.addToBackStack(tag); + } + transaction.commit(); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressHandle(); + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + onBackPressHandle(); + } + + private void onBackPressHandle() { + Fragment editScanFragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_EDIT_SCAN_TAG); + Fragment cropScanFragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_CROP_SCAN_TAG); + Fragment saveScanFragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_SAVE_SCAN_TAG); + if (cropScanFragment != null || saveScanFragment != null) { + int index = 0; + if (cropScanFragment instanceof CropScannedDocumentFragment) { + index = ((CropScannedDocumentFragment) cropScanFragment).getScannedDocIndex(); + } + onReplaceFragment(EditScannedDocumentFragment.newInstance(index), FRAGMENT_EDIT_SCAN_TAG, false); + } else if (editScanFragment != null) { + createScanFragment(null); + } else { + super.onBackPressed(); + } + } + + private void initScanbotSDK() { + scanbotSDK = new ScanbotSDK(this); + } + + public ScanbotSDK getScanbotSDK() { + return scanbotSDK; + } + + @Override + public void addScannedDoc(Bitmap file) { + if (file != null) { + originalScannedImages.add(file); + filteredImages.add(file); + scannedImagesFilterIndex.add(0);//no filter by default + } + } + + @Override + public List getScannedDocs() { + return filteredImages; + } + + @Override + public boolean removedScannedDoc(Bitmap file, int index) { + //removed the filter applied index also when scanned document is removed + if (scannedImagesFilterIndex.size() > 0 && scannedImagesFilterIndex.size() > index) { + scannedImagesFilterIndex.remove(index); + } + if (originalScannedImages.size() > 0 && file != null) { + originalScannedImages.remove(index); + } + if (filteredImages.size() > 0 && file != null) { + filteredImages.remove(index); + return true; + } + return false; + } + + @Override + public Bitmap replaceScannedDoc(int index, Bitmap newFile, boolean isFilterApplied) { + //only update the original bitmap if no filter is applied + if (!isFilterApplied && originalScannedImages.size() > 0 && newFile != null && index >= 0 && originalScannedImages.size() - 1 >= index) { + originalScannedImages.set(index, newFile); + } + if (filteredImages.size() > 0 && newFile != null && index >= 0 && filteredImages.size() - 1 >= index) { + return filteredImages.set(index, newFile); + } + return null; + } + + @Override + public void replaceFilterIndex(int index, int filterIndex) { + if (scannedImagesFilterIndex.size() > 0 && scannedImagesFilterIndex.size() > index) { + scannedImagesFilterIndex.set(index, filterIndex); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + List fragmentList = getSupportFragmentManager().getFragments(); + for (Fragment fragment : fragmentList) { + fragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + protected void checkAndCreateFolderIfRequired() { + //if user is coming from sub-folder then we should not check for existence as folder will be available + if (!TextUtils.isEmpty(remotePath) && !remotePath.equals(OCFile.ROOT_PATH)) { + return; + } + + //no need to do any operation if its already finished earlier + if (isFolderCheckOperationFinished) { + return; + } + + String lastRemotePath = appPreferences.getUploadScansLastPath(); + + //create the default scan folder if it doesn't exist or if user has not selected any other folder + if (lastRemotePath.equalsIgnoreCase(ScanActivity.DEFAULT_UPLOAD_SCAN_PATH)) { + getFileOperationsHelper().createFolderIfNotExist(lastRemotePath, false); + return; + } + + //if last saved remote path is not root path then we have to check if the folder exist or not + if (!lastRemotePath.equals(OCFile.ROOT_PATH)) { + getFileOperationsHelper().createFolderIfNotExist(lastRemotePath, true); + } + } + + @Override + public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { + super.onRemoteOperationFinish(operation, result); + if (operation instanceof 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.getCode() == RemoteOperationResult.ResultCode.FILE_NOT_FOUND) { + Fragment saveScanFragment = getSupportFragmentManager().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.setUploadScansLastPath(OCFile.ROOT_PATH); + //if folder doesn't exist then we have to set the remote path as root i.e. fallback mechanism + ((SaveScannedDocumentFragment) saveScanFragment).setRemoteFilePath(OCFile.ROOT_PATH); + } + } + isFolderCheckOperationFinished = true; + } + } +} diff --git a/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt b/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt new file mode 100644 index 000000000000..3fb7e57a8e71 --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt @@ -0,0 +1,459 @@ +package com.nmc.android.ui + +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.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.Fragment +import com.nmc.android.interfaces.OnDocScanListener +import com.nmc.android.interfaces.OnFragmentChangeListener +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentScanDocumentBinding +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.camera.CaptureInfo +import io.scanbot.sdk.camera.FrameHandlerResult +import io.scanbot.sdk.contourdetector.ContourDetectorFrameHandler +import io.scanbot.sdk.core.contourdetector.ContourDetector +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.docdetection.ui.IDocumentScannerViewCallback +import io.scanbot.sdk.docprocessing.PageProcessor +import io.scanbot.sdk.ocr.OpticalCharacterRecognizer +import io.scanbot.sdk.persistence.PageFileStorage +import io.scanbot.sdk.process.CropOperation +import io.scanbot.sdk.process.ImageProcessor +import io.scanbot.sdk.ui.camera.CameraUiSettings +import io.scanbot.sdk.ui.view.base.configuration.CameraOrientationMode + +class ScanDocumentFragment : Fragment() { + + private lateinit var scanbotSDK: ScanbotSDK + private lateinit var contourDetector: ContourDetector + private lateinit var imageProcessor: ImageProcessor + + private var lastUserGuidanceHintTs = 0L + private var flashEnabled = false + private var autoSnappingEnabled = true + private val ignoreBadAspectRatio = true + + //OCR + private lateinit var opticalCharacterRecognizer: OpticalCharacterRecognizer + private lateinit var pageFileStorage: PageFileStorage + private lateinit var pageProcessor: PageProcessor + + private lateinit var onDocScanListener: OnDocScanListener + private lateinit var onFragmentChangeListener: OnFragmentChangeListener + + private lateinit var calledFrom: String + + private lateinit var binding: FragmentScanDocumentBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.getString(ARG_CALLED_FROM)?.let { + calledFrom = it + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + try { + onDocScanListener = context as OnDocScanListener + onFragmentChangeListener = context as OnFragmentChangeListener + } catch (ignored: Exception) { + + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if (requireActivity() is ScanActivity) { + (requireActivity() as ScanActivity).showHideToolbar(false) + (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(false) + } + binding = FragmentScanDocumentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + askPermission() + initDependencies() + + binding.camera.apply { + initCamera(CameraUiSettings(true)) + initDetectionBehavior(contourDetector, + { result -> + // Here you are continuously notified about contour detection results. + // For example, you can show a user guidance text depending on the current detection status. + // don't update the text if fragment is removing + if (!isRemoving) { + // Here you are continuously notified about contour detection results. + // For example, you can show a user guidance text depending on the current detection status. + binding.userGuidanceHint.post { + if (result is FrameHandlerResult.Success<*>) { + showUserGuidance((result as FrameHandlerResult.Success).value.detectionStatus) + } + } + } + false // typically you need to return false + }, + object : IDocumentScannerViewCallback { + override fun onCameraOpen() { + // In this example we demonstrate how to lock the orientation of the UI (Activity) + // as well as the orientation of the taken picture to portrait. + binding.camera.cameraConfiguration.setCameraOrientationMode(CameraOrientationMode.PORTRAIT) + + binding.camera.viewController.useFlash(flashEnabled) + binding.camera.viewController.continuousFocus() + } + + override fun onPictureTaken(image: ByteArray, captureInfo: CaptureInfo) { + processPictureTaken(image, captureInfo.imageOrientation) + + // continue scanning + /*binding.camera.postDelayed({ + binding.camera.viewController.startPreview() + }, 1000)*/ + } + } + ) + + // See https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/using-scanbot-camera-view/#preview-mode + // cameraConfiguration.setCameraPreviewMode(io.scanbot.sdk.camera.CameraPreviewMode.FIT_IN) + } + + binding.camera.viewController.apply { + setAcceptedAngleScore(60.0) + setAcceptedSizeScore(75.0) + setIgnoreBadAspectRatio(ignoreBadAspectRatio) + + // Please note: https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/autosnapping/#sensitivity + setAutoSnappingSensitivity(0.85f) + } + + binding.shutterButton.setOnClickListener { binding.camera.viewController.takePicture(false) } + binding.shutterButton.visibility = View.VISIBLE + + binding.scanDocBtnFlash.setOnClickListener { + flashEnabled = !flashEnabled + binding.camera.viewController.useFlash(flashEnabled) + toggleFlashButtonUI() + } + binding.scanDocBtnCancel.setOnClickListener { + // if fragment opened from Edit Scan Fragment then on cancel click it should go to that fragment + if (calledFrom == EditScannedDocumentFragment.TAG) { + openEditScanFragment() + } else { + // else default behaviour + (requireActivity() as ScanActivity).onBackPressed() + } + } + + binding.scanDocBtnAutomatic.setOnClickListener { + autoSnappingEnabled = !autoSnappingEnabled + setAutoSnapEnabled(autoSnappingEnabled) + } + binding.scanDocBtnAutomatic.post { setAutoSnapEnabled(autoSnappingEnabled) } + + toggleFlashButtonUI() + } + + private fun toggleFlashButtonUI() { + if (flashEnabled) { + binding.scanDocBtnFlash.setIconTintResource(R.color.primary) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) + } else { + binding.scanDocBtnFlash.setIconTintResource(R.color.grey_60) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) + } + } + + private fun askPermission() { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + || ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.READ_EXTERNAL_STORAGE + ) != PackageManager + .PERMISSION_GRANTED || ContextCompat.checkSelfPermission( + requireActivity(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != + PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + requireActivity(), arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + CAMERA_PERMISSION_REQUEST_CODE + ) + } + } + + private fun initDependencies() { + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + contourDetector = scanbotSDK.createContourDetector() + imageProcessor = scanbotSDK.imageProcessor() + opticalCharacterRecognizer = scanbotSDK.createOcrRecognizer() + pageFileStorage = scanbotSDK.createPageFileStorage() + pageProcessor = scanbotSDK.createPageProcessor() + } + + override fun onResume() { + super.onResume() + binding.camera.viewController.onResume() + binding.scanDocProgressBar.visibility = View.GONE + } + + override fun onPause() { + super.onPause() + binding.camera.viewController.onPause() + } + + private fun showUserGuidance(result: DetectionStatus) { + if (!autoSnappingEnabled) { + return + } + if (System.currentTimeMillis() - lastUserGuidanceHintTs < 400) { + return + } + + // Make sure to reset the default polygon fill color (see the ignoreBadAspectRatio case). + // polygonView.setFillColor(POLYGON_FILL_COLOR) + // fragment should be added and visible because this method is being called from handler + // it can be called when fragment is not attached or visible + if (isAdded && isVisible) { + when (result) { + DetectionStatus.OK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.OK_BUT_TOO_SMALL -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_move_closer) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.OK_BUT_BAD_ANGLES -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_perspective) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.ERROR_NOTHING_DETECTED -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_no_doc) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.ERROR_TOO_NOISY -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_bg_noisy) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + if (ignoreBadAspectRatio) { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + // change polygon color to "OK" + // polygonView.setFillColor(POLYGON_FILL_COLOR_OK) + } else { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_aspect_ratio) + } + binding.userGuidanceHint.visibility = View.VISIBLE + } + + DetectionStatus.ERROR_TOO_DARK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_poor_light) + binding.userGuidanceHint.visibility = View.VISIBLE + } + + else -> binding.userGuidanceHint.visibility = View.GONE + } + } + lastUserGuidanceHintTs = System.currentTimeMillis() + } + + private fun processPictureTaken(image: ByteArray, imageOrientation: Int) { + requireActivity().runOnUiThread { + binding.camera.viewController.onPause() + binding.scanDocProgressBar.visibility = View.VISIBLE + //cameraView.visibility = View.GONE + } + // Here we get the full image from the camera. + // Please see https://github.com/doo/Scanbot-SDK-Examples/wiki/Handling-camera-picture + // This is just a demo showing the detected document image as a downscaled(!) preview image. + + // Decode Bitmap from bytes of original image: + val options = BitmapFactory.Options() + // Please note: In this simple demo we downscale the original image to 1/8 for the preview! + //options.inSampleSize = 8 + // Typically you will need the full resolution of the original image! So please change the "inSampleSize" value to 1! + options.inSampleSize = 1 + var originalBitmap = BitmapFactory.decodeByteArray(image, 0, image.size, options) + + // Rotate the original image based on the imageOrientation value. + // Required for some Android devices like Samsung! + if (imageOrientation > 0) { + val matrix = Matrix() + matrix.setRotate(imageOrientation.toFloat(), originalBitmap.width / 2f, originalBitmap.height / 2f) + originalBitmap = Bitmap.createBitmap( + originalBitmap, + 0, + 0, + originalBitmap.width, + originalBitmap.height, + matrix, + false + ) + } + + // Run document detection on original image: + val result = contourDetector.detect(originalBitmap)!! + val detectedPolygon = result.polygonF + + val documentImage = imageProcessor.processBitmap(originalBitmap, CropOperation(detectedPolygon), false) + + // val file = saveImage(documentImage) + // Log.d("SCANNING","File : $file") + if (documentImage != null) { + onDocScanListener.addScannedDoc(documentImage) + // onDocScanListener.addScannedDoc(FileUtils.saveImage(requireContext(), documentImage, null)) + openEditScanFragment() + + /* uiScope.launch { + recognizeTextWithoutPDFTask(documentImage) + }*/ + } + // RecognizeTextWithoutPDFTask(documentImage).execute() + + //resultView.post { resultView.setImageBitmap(documentImage) } + + // continue scanning + /* cameraView.postDelayed({ + cameraView.continuousFocus() + cameraView.startPreview() + }, 1000)*/ + } + + private fun openEditScanFragment() { + onFragmentChangeListener.onReplaceFragment( + EditScannedDocumentFragment.newInstance(onDocScanListener.scannedDocs.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 + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission is granted + // Nothing to be done + } else { + // permission not granted + for (permission in permissions) { + val showRationale = shouldShowRequestPermissionRationale(permission) + 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)) + break + } else if (Manifest.permission.CAMERA == permission || Manifest.permission.READ_EXTERNAL_STORAGE == permission + || Manifest.permission.WRITE_EXTERNAL_STORAGE == permission + ) { + // 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)) + break + // askPermission() + } + // else if ( /* possibly check more permissions...*/) { + // } + } + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + 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 { + private const val CAMERA_PERMISSION_REQUEST_CODE: Int = 811 + + @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/ui/ScanPagerFragment.java b/app/src/main/java/com/nmc/android/ui/ScanPagerFragment.java new file mode 100644 index 000000000000..f56ca2ee5477 --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/ScanPagerFragment.java @@ -0,0 +1,213 @@ +package com.nmc.android.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.nmc.android.interfaces.OnDocScanListener; +import com.nmc.android.utils.ScanBotSdkUtils; +import com.owncloud.android.R; +import com.owncloud.android.databinding.ItemScannedDocBinding; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.os.HandlerCompat; +import androidx.fragment.app.Fragment; +import io.scanbot.sdk.ScanbotSDK; +import io.scanbot.sdk.process.FilterOperation; +import io.scanbot.sdk.process.ImageFilterType; +import io.scanbot.sdk.process.RotateOperation; + +public class ScanPagerFragment extends Fragment { + + private static final String ARG_SCANNED_DOC_PATH = "scanned_doc_path"; + + public ScanPagerFragment() { + } + + public static ScanPagerFragment newInstance(int i) { + + Bundle args = new Bundle(); + args.putInt(ARG_SCANNED_DOC_PATH, i); + + ScanPagerFragment fragment = new ScanPagerFragment(); + fragment.setArguments(args); + return fragment; + } + + private ItemScannedDocBinding binding; + + private ScanbotSDK scanbotSDK; + private Bitmap originalBitmap; + private Bitmap previewBitmap; + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); + + private long lastRotationEventTs = 0L; + private int rotationDegrees = 0; + private int index; + + private OnDocScanListener onDocScanListener; + private AlertDialog applyFilterDialog; + private int selectedFilter = 0; + + //flag to check if applying filter is in progress or not + private boolean isFilterApplyInProgress = false; + + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + try { + onDocScanListener = (OnDocScanListener) context; + } catch (Exception ignored) { + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + index = getArguments().getInt(ARG_SCANNED_DOC_PATH); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (requireActivity() instanceof ScanActivity) { + scanbotSDK = ((ScanActivity) requireActivity()).getScanbotSDK(); + } + binding = ItemScannedDocBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + //File file = new File(scannedDocPath); + //originalBitmap = FileUtils.convertFileToBitmap(file); + // previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap); + // loadImage(); + setUpBitmap(); + } + + private void setUpBitmap() { + executorService.execute(() -> { + if (index >= 0 && index < ScanActivity.filteredImages.size()) { + originalBitmap = onDocScanListener.getScannedDocs().get(index); + previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap); + } + if (index >= 0 && index < ScanActivity.scannedImagesFilterIndex.size()) { + selectedFilter = ScanActivity.scannedImagesFilterIndex.get(index); + } + handler.post(() -> loadImage()); + }); + } + + private void loadImage() { + if (binding != null) { + if (previewBitmap != null) { + binding.editScannedImageView.setImageBitmap(previewBitmap); + } else if (originalBitmap != null) { + binding.editScannedImageView.setImageBitmap(originalBitmap); + } + } + } + + @Override + public void onDestroyView() { + binding = null; + + super.onDestroyView(); + + if (applyFilterDialog != null && applyFilterDialog.isShowing()) { + applyFilterDialog.dismiss(); + } + } + + public void rotate() { + if (System.currentTimeMillis() - lastRotationEventTs < 350) { + return; + } + rotationDegrees += 90; + binding.editScannedImageView.rotateClockwise(); + lastRotationEventTs = System.currentTimeMillis(); + executorService.execute(() -> { + Bitmap rotatedBitmap = scanbotSDK.imageProcessor().processBitmap(originalBitmap, + new ArrayList<>(Collections.singletonList(new RotateOperation(rotationDegrees))), false); + onDocScanListener.replaceScannedDoc(index, rotatedBitmap, false); + }); + } + + public void showApplyFilterDialog() { + String[] filterArray = getResources().getStringArray(R.array.edit_scan_filter_values); + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); + builder.setTitle(R.string.edit_scan_filter_dialog_title) + .setSingleChoiceItems(filterArray, + selectedFilter, + (dialog, which) -> { + selectedFilter = which; + onDocScanListener.replaceFilterIndex(index, selectedFilter); + if (filterArray[which].equalsIgnoreCase(getResources().getString(R.string.edit_scan_filter_none))) { + applyFilter(ImageFilterType.NONE); + } else if (filterArray[which].equalsIgnoreCase(getResources().getString(R.string.edit_scan_filter_pure_binarized))) { + applyFilter(ImageFilterType.PURE_BINARIZED); + } else if (filterArray[which].equalsIgnoreCase(getResources().getString(R.string.edit_scan_filter_color_enhanced))) { + applyFilter(ImageFilterType.COLOR_ENHANCED, ImageFilterType.EDGE_HIGHLIGHT); + } else if (filterArray[which].equalsIgnoreCase(getResources().getString(R.string.edit_scan_filter_color_document))) { + applyFilter(ImageFilterType.COLOR_DOCUMENT); + } else if (filterArray[which].equalsIgnoreCase(getResources().getString(R.string.edit_scan_filter_grey))) { + applyFilter(ImageFilterType.GRAYSCALE); + } else if (filterArray[which].equalsIgnoreCase(getResources().getString(R.string.edit_scan_filter_b_n_w))) { + applyFilter(ImageFilterType.BLACK_AND_WHITE); + } + + dialog.dismiss(); + }) + .setOnCancelListener(dialog -> { + }); + applyFilterDialog = builder.create(); + applyFilterDialog.show(); + } + + private void applyFilter(ImageFilterType... imageFilterType) { + binding.editScanImageProgressBar.setVisibility(View.VISIBLE); + isFilterApplyInProgress = true; + executorService.execute(() -> { + if (imageFilterType[0] != ImageFilterType.NONE) { + List filterOperationList = new ArrayList<>(); + for (ImageFilterType filters : imageFilterType) { + filterOperationList.add(new FilterOperation(filters)); + } + previewBitmap = scanbotSDK.imageProcessor().processBitmap(originalBitmap, filterOperationList, false); + } else { + previewBitmap = ScanActivity.originalScannedImages.get(index); + } + onDocScanListener.replaceScannedDoc(index, previewBitmap, true); + handler.post(() -> { + isFilterApplyInProgress = false; + binding.editScanImageProgressBar.setVisibility(View.GONE); + loadImage(); + }); + }); + } + + //scan should not be saved till filter is applied + public boolean isFilterApplyInProgress() { + return isFilterApplyInProgress; + } +} 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..fb638c5db2d6 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/FileUtils.java @@ -0,0 +1,159 @@ +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 android.util.Log; + +import com.owncloud.android.MainApp; +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.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +// 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 SCANNED_FILE_PREFIX = "scan_"; + private static final int JPG_FILE_TYPE = 1; + private static final int PNG_FILE_TYPE = 2; + + public static File saveJpgImage(Context context, Bitmap bitmap, String imageName, int quality) { + return createFileAndSaveImage(context, bitmap, imageName, quality, JPG_FILE_TYPE); + } + + public static File savePngImage(Context context, Bitmap bitmap, String imageName, int quality) { + return createFileAndSaveImage(context, bitmap, imageName, quality, PNG_FILE_TYPE); + } + + private static File createFileAndSaveImage(Context context, Bitmap bitmap, String imageName, int quality, + int fileType) { + File file = fileType == PNG_FILE_TYPE ? getPngImageName(context, imageName) : getJpgImageName(context, + imageName); + return saveImage(file, bitmap, quality, fileType); + } + + private static File saveImage(File file, Bitmap bitmap, int quality, int fileType) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos); + byte[] bitmapData = bos.toByteArray(); + + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(bitmapData); + fileOutputStream.flush(); + fileOutputStream.close(); + return file; + } catch (Exception e) { + Log_OC.e(TAG, " Failed to save image : " + e.getLocalizedMessage()); + return null; + } + } + + private static File getJpgImageName(Context context, String imageName) { + File imageFile = getOutputMediaFile(context); + if (!TextUtils.isEmpty(imageName)) { + return new File(imageFile.getPath() + File.separator + imageName + ".jpg"); + } else { + return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName()); + } + } + + private static File getPngImageName(Context context, String imageName) { + File imageFile = getOutputMediaFile(context); + if (!TextUtils.isEmpty(imageName)) { + return new File(imageFile.getPath() + File.separator + imageName + ".png"); + } else { + return new File(imageFile.getPath() + File.separator + "IMG_" + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".png")); + } + } + + private static File getTextFileName(Context context, String fileName) { + File txtFileName = getOutputMediaFile(context); + if (!TextUtils.isEmpty(fileName)) { + return new File(txtFileName.getPath() + File.separator + fileName + ".txt"); + } else { + return new File(txtFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".jpg", ".txt")); + } + } + + private static File getPdfFileName(Context context, String fileName) { + File pdfFileName = getOutputMediaFile(context); + if (!TextUtils.isEmpty(fileName)) { + return new File(pdfFileName.getPath() + File.separator + fileName + ".pdf"); + } else { + return new File(pdfFileName.getPath() + File.separator + FileOperationsHelper.getCapturedImageName().replace(".pdf", ".txt")); + } + } + + public static String scannedFileName() { + return SCANNED_FILE_PREFIX + new SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US).format(new Date()); + } + + public static File getOutputMediaFile(Context context) { + File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), ""); + 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; + + } + + + /** + * delete all the files inside the pictures directory + * this directory is getting used to store the scanned images temporarily till they uploaded to cloud + * the scanned files after downloading will get deleted by UploadWorker but in case some files still there + * then we have to delete it when user do logout from the app + * @param context + */ + public static void deleteFilesFromPicturesDirectory(Context context) { + File getFileDirectory = getOutputMediaFile(context); + if (getFileDirectory.isDirectory()) { + File[] fileList = getFileDirectory.listFiles(); + if (fileList != null && fileList.length > 0) { + for (File file : fileList) { + file.delete(); + } + } + } + } + +} diff --git a/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java b/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java new file mode 100644 index 000000000000..ec43ac2dd3a8 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java @@ -0,0 +1,21 @@ +package com.nmc.android.utils; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +public class KeyboardUtils { + + public static void showSoftKeyboard(Context context, View view) { + view.requestFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + + public static void hideKeyboardFrom(Context context, View view) { + view.clearFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/ScanBotSdkUtils.kt b/app/src/main/java/com/nmc/android/utils/ScanBotSdkUtils.kt new file mode 100644 index 000000000000..c1a301019bf5 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/ScanBotSdkUtils.kt @@ -0,0 +1,54 @@ +package com.nmc.android.utils + +import android.app.Activity +import android.graphics.Bitmap +import com.owncloud.android.lib.common.utils.Log_OC +import io.scanbot.sdk.ScanbotSDK +import kotlin.math.roundToInt + +object ScanBotSdkUtils { + private val TAG = ScanBotSdkUtils::class.java.simpleName + + //license key will be valid for application id: com.t_systems.android.webdav & com.t_systems.android.webdav.beta + //License validity till 25th March 2025 + const val LICENSE_KEY = "Z+GkfzcfWnNtCoGp4OsH9EJg4OuN5v" + + "BDhPFzHkhecpQaOS4s/r3qRPvKtgpG" + + "Q89KqfbvPC9Bwx/rPE7GYMmh+YnFIV" + + "wMD3HcGr4X0ETbH8JdsVP7njFJ5+yi" + + "xqlS3aSBh3GWtKT+umoTAzXbqF0ZS/" + + "EGXg0AhwWpQ7Fp+fyNMLwJTxt9/6Ya" + + "MZ2C0+MVwZyauKjeglILGZrcfenFR+" + + "a1LjBexcBigcqpMqsd6pDIBwtdp8RY" + + "spCuYgyQ6Vfb+DYbPts6ynFxXR1bsq" + + "TRcWBfkVMXIyCSNqgGStHCOZlVvqKo" + + "anolbemQEGz9lDtigeQN/4txtKX0L9" + + "2PLfqq6rOh/w==\nU2NhbmJvdFNESw" + + "pjb20udF9zeXN0ZW1zLmFuZHJvaWQu" + + "d2ViZGF2fGNvbS50X3N5c3RlbXMuYW" + + "5kcm9pZC53ZWJkYXYuYmV0YQoxNzQ1" + + "NjI1NTk5CjExNTU2NzgKMg==\n" + + @JvmStatic + fun isScanBotLicenseValid(activity: Activity): Boolean { + // Check the license status: + val licenseInfo = ScanbotSDK(activity).licenseInfo + Log_OC.d(TAG, "License status: ${licenseInfo.status}") + Log_OC.d(TAG, "License isValid: ${licenseInfo.isValid}") + + // Making your call into ScanbotSDK API is safe now. + // e.g. start barcode scanner + return licenseInfo.isValid + } + + @JvmStatic + fun resizeForPreview(bitmap: Bitmap): Bitmap { + val maxW = 1000f + val maxH = 1000f + val oldWidth = bitmap.width.toFloat() + val oldHeight = bitmap.height.toFloat() + val scaleFactor = if (oldWidth > oldHeight) maxW / oldWidth else maxH / oldHeight + val scaledWidth = (oldWidth * scaleFactor).roundToInt() + val scaledHeight = (oldHeight * scaleFactor).roundToInt() + return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index f6d754b402ab..01425ede9b9f 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -57,6 +57,7 @@ import com.nextcloud.client.preferences.DarkMode; import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.authentication.AuthenticatorActivity; +import com.nmc.android.utils.ScanBotSdkUtils; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -115,6 +116,9 @@ import de.cotech.hw.SecurityKeyManager; import de.cotech.hw.SecurityKeyManagerConfig; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.scanbot.sap.SdkFeature; +import io.scanbot.sdk.ScanbotSDKInitializer; +import io.scanbot.sdk.core.contourdetector.ContourDetector; import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP; @@ -356,6 +360,8 @@ public void onCreate() { backgroundJobManager.schedulePeriodicHealthStatus(); registerGlobalPassCodeProtection(); + + initialiseScanBotSDK(); } private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> { @@ -623,6 +629,10 @@ public static void notificationChannels() { createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string .notification_channel_general_name, R.string.notification_channel_general_description, context, NotificationManager.IMPORTANCE_DEFAULT); + + createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_IMAGE_SAVE, + R.string.notification_channel_image_save, + R.string.notification_channel_image_save_description, context); } else { Log_OC.e(TAG, "Notification manager is null"); } @@ -917,4 +927,25 @@ public static void setAppTheme(DarkMode mode) { case SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); } } + + /** + * method to initialise the ScanBot SDK + */ + private void initialiseScanBotSDK() { + new ScanbotSDKInitializer() + .withLogging(BuildConfig.DEBUG, BuildConfig.DEBUG) + .license(this, ScanBotSdkUtils.LICENSE_KEY) + .contourDetectorType(ContourDetector.Type.ML_BASED) // ML_BASED is default. Set it to EDGE_BASED to use the edge-based approach + .licenceErrorHandler((status, sdkFeature, statusMessage) -> { + // Handle license errors here: + Log_OC.d(TAG, "License status: " + status.name()); + if (sdkFeature != SdkFeature.NoSdkFeature) { + Log_OC.d(TAG, "Missing SDK feature in license: " + sdkFeature.name()); + } + }) + //enable sdkFilesDir if custom file directory has to be set + //.sdkFilesDirectory(this,getExternalFilesDir(null)) + .prepareOCRLanguagesBlobs(true) + .initialize(this); + } } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderIfNotExistOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderIfNotExistOperation.java new file mode 100644 index 000000000000..25ecc07541d7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderIfNotExistOperation.java @@ -0,0 +1,72 @@ +/* + * ownCloud Android client application + * + * @author masensio + * @author Andy Scherzinger + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2018 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.operations; + +import android.content.Context; + +import com.nextcloud.client.account.User; +import com.owncloud.android.datamodel.FileDataStorageManager; +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation; +import com.owncloud.android.operations.common.SyncOperation; + +/** + * create folder only if it doesn't exist in remote + */ +public class CreateFolderIfNotExistOperation extends SyncOperation { + + private static final String TAG = CreateFolderIfNotExistOperation.class.getSimpleName(); + + private final String mRemotePath; + private final User user; + private final Context context; + private boolean isCheckOnlyFolderExistence; //flag to check if folder exist or not and will not create folder if flag is true + + public CreateFolderIfNotExistOperation(String remotePath, User user, boolean isCheckOnlyFolderExistence, Context context, FileDataStorageManager storageManager) { + super(storageManager); + this.mRemotePath = remotePath; + this.user = user; + this.context = context; + this.isCheckOnlyFolderExistence = isCheckOnlyFolderExistence; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + RemoteOperation operation = new ExistenceCheckRemoteOperation(mRemotePath, false); + RemoteOperationResult result = operation.execute(client); + + if (isCheckOnlyFolderExistence) { + return result; + } + + //if remote folder doesn't exist then create it else ignore it + if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND) { + SyncOperation syncOp = new CreateFolderOperation(mRemotePath, user, context, getStorageManager()); + result = syncOp.execute(client); + } + + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index cb3d53642968..1f73c317bb23 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -48,6 +48,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; @@ -99,6 +100,7 @@ public class OperationsService extends Service { public static final String EXTRA_SHARE_ID = "SHARE_ID"; public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE"; public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND"; + public static final String EXTRA_CHECK_ONLY_FOLDER_EXISTENCE = "CHECK_ONLY_FOLDER_EXISTENCE"; public static final String ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK"; public static final String ACTION_CREATE_SECURE_FILE_DROP = "CREATE_SECURE_FILE_DROP"; @@ -119,6 +121,7 @@ public class OperationsService extends Service { public static final String ACTION_COPY_FILE = "COPY_FILE"; public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS"; public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION"; + public static final String ACTION_CREATE_FOLDER_NOT_EXIST = "CREATE_FOLDER_NOT_EXIST"; private ServiceHandler mOperationsHandler; private OperationsServiceBinder mOperationsBinder; @@ -690,6 +693,13 @@ private Pair newOperation(Intent operationIntent) { fileDataStorageManager); break; + case ACTION_CREATE_FOLDER_NOT_EXIST: + remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + operation = new CreateFolderIfNotExistOperation(remotePath, user, + operationIntent.getBooleanExtra(EXTRA_CHECK_ONLY_FOLDER_EXISTENCE, false), + getApplicationContext(), fileDataStorageManager); + break; + case ACTION_SYNC_FILE: remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 4e7496dc62ec..82d863a33238 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -69,6 +69,7 @@ import com.owncloud.android.databinding.FilesBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.nmc.android.ui.SaveScannedDocumentFragment; import com.owncloud.android.datamodel.VirtualFolderType; import com.owncloud.android.files.services.NameCollisionPolicy; import com.owncloud.android.lib.common.OwnCloudClient; @@ -194,6 +195,7 @@ public class FileDisplayActivity extends FileActivity public static final int REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM = REQUEST_CODE__LAST_SHARED + 2; public static final int REQUEST_CODE__MOVE_OR_COPY_FILES = REQUEST_CODE__LAST_SHARED + 3; public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5; + public static final int REQUEST_CODE__SCAN_DOCUMENT = REQUEST_CODE__LAST_SHARED + 6; protected static final long DELAY_TO_REQUEST_REFRESH_OPERATION_LATER = DELAY_TO_REQUEST_OPERATIONS_LATER + 350; @@ -859,6 +861,26 @@ public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String. exitSelectionMode(); } else if (requestCode == PermissionUtil.REQUEST_CODE_MANAGE_ALL_FILES) { syncAndUpdateFolder(true); + } else if (requestCode == REQUEST_CODE__SCAN_DOCUMENT && resultCode == RESULT_OK) { + + OCFile remoteFilePath = data.getParcelableExtra(SaveScannedDocumentFragment.EXTRA_SCAN_DOC_REMOTE_PATH); + if (remoteFilePath == null) { + remoteFilePath = getCurrentDir(); + } + + Log_OC.d(this, "Scan Document save remote path: " + remoteFilePath.getRemotePath()); + + // NMC-2418 fix + if (remoteFilePath.getRemotePath().equals(getCurrentDir().getRemotePath())) { + Log_OC.d(this, "Both current and scan paths are same. Skipping redirection."); + return; + } + + OCFileListFragment fileListFragment = getListOfFilesFragment(); + if (fileListFragment != null) { + fileListFragment.onItemClicked(remoteFilePath); + } + } else { super.onActivityResult(requestCode, resultCode, data); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 0961de2671b8..b748729b530c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -60,6 +60,8 @@ open class FolderPickerActivity : private var mSyncBroadcastReceiver: SyncBroadcastReceiver? = null private var mSyncInProgress = false private var mSearchOnlyFolders = false + private var mShowOnlyFolder = false + private var mHideEncryptedFolder = false var isDoNotEnterEncryptedFolder = false private set @@ -113,12 +115,27 @@ open class FolderPickerActivity : } private fun setupAction() { + mShowOnlyFolder = intent.getBooleanExtra(EXTRA_SHOW_ONLY_FOLDER, false) + mHideEncryptedFolder = intent.getBooleanExtra(EXTRA_HIDE_ENCRYPTED_FOLDER, false) + action = intent.getStringExtra(EXTRA_ACTION) - if (action != null && action == CHOOSE_LOCATION) { - setupUIForChooseButton() + if (action != null) { + when (action) { + MOVE_OR_COPY -> { + captionText = resources.getText(R.string.folder_picker_choose_caption_text).toString() + mSearchOnlyFolders = true + isDoNotEnterEncryptedFolder = true + } + + CHOOSE_LOCATION -> { + setupUIForChooseButton() + } + + else -> configureDefaultCase() + } } else { - captionText = themeUtils.getDefaultDisplayNameForRootFolder(this) + configureDefaultCase() } } @@ -127,9 +144,10 @@ open class FolderPickerActivity : } private fun setupUIForChooseButton() { - captionText = resources.getText(R.string.folder_picker_choose_caption_text).toString() + captionText = resources.getText(R.string.choose_location).toString() mSearchOnlyFolders = true isDoNotEnterEncryptedFolder = true + mShowOnlyFolder = true if (this is FilePickerActivity) { return @@ -137,11 +155,18 @@ open class FolderPickerActivity : folderPickerBinding.folderPickerBtnCopy.visibility = View.GONE folderPickerBinding.folderPickerBtnMove.visibility = View.GONE folderPickerBinding.folderPickerBtnChoose.visibility = View.VISIBLE - folderPickerBinding.chooseButtonSpacer.visibility = View.VISIBLE folderPickerBinding.moveOrCopyButtonSpacer.visibility = View.GONE + + // NMC Customization + folderPickerBinding.folderPickerBtnChoose.text = resources.getString(R.string.common_select) } } + // NMC Customization + private fun configureDefaultCase() { + captionText = themeUtils.getDefaultDisplayNameForRootFolder(this) + } + private fun handleOnBackPressed() { onBackPressedDispatcher.addCallback( this, @@ -358,7 +383,7 @@ open class FolderPickerActivity : } private fun refreshListOfFilesFragment(fromSearch: Boolean) { - listOfFilesFragment?.listDirectory(false, fromSearch) + listOfFilesFragment?.listDirectoryFolder(false, fromSearch, mShowOnlyFolder, mHideEncryptedFolder) } fun browseToRoot() { @@ -660,6 +685,12 @@ open class FolderPickerActivity : @JvmField val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION") + @JvmField + val EXTRA_SHOW_ONLY_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_SHOW_ONLY_FOLDER") + + @JvmField + val EXTRA_HIDE_ENCRYPTED_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_HIDE_ENCRYPTED_FOLDER") + const val MOVE_OR_COPY = "MOVE_OR_COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" private val TAG = FolderPickerActivity::class.java.simpleName diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt index 350c576ea37a..ff308dee169e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -64,6 +64,7 @@ class NotificationsActivity : DrawerActivity(), NotificationsContract.View { } setupToolbar() + showHideDefaultToolbarDivider(true) updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications)) setupDrawer(R.id.nav_notifications) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index b018647ed0e2..376fdf54ee8a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java @@ -62,6 +62,7 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable private LinearLayout mInfoBox; private TextView mInfoBoxMessage; protected AppCompatSpinner mToolbarSpinner; + private View mDefaultToolbarDivider; private boolean isHomeSearchToolbarShow = false; @Inject public ThemeColorUtils themeColorUtils; @@ -82,6 +83,7 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mMenuButton = findViewById(R.id.menu_button); mSearchText = findViewById(R.id.search_text); mSwitchAccountButton = findViewById(R.id.switch_account_button); + mDefaultToolbarDivider = findViewById(R.id.default_toolbar_divider); if (showSortListButtonGroup) { findViewById(R.id.sort_list_button_group).setVisibility(View.VISIBLE); @@ -180,6 +182,14 @@ private void showHomeSearchToolbar(boolean isShow) { } } + public void showHideToolbar(boolean isShow){ + mDefaultToolbar.setVisibility(isShow ? View.VISIBLE : View.GONE); + } + + public void showHideDefaultToolbarDivider(boolean isShow) { + mDefaultToolbarDivider.setVisibility(isShow ? View.VISIBLE : View.GONE); + } + /** * Updates title bar and home buttons (state and icon). */ diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index cd99bf1a2dc7..2b227f4c49fe 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -65,6 +65,7 @@ import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; +import com.owncloud.android.utils.MimeType; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -757,6 +758,65 @@ public void swapDirectory( notifyDataSetChanged(); } + /** + * method will only called if only folders has to show + */ + public void showOnlyFolder( + User account, + OCFile directory, + FileDataStorageManager updatedStorageManager, + boolean onlyOnDevice, String limitToMimeType, + boolean hideEncryptedFolder + ) { + this.onlyOnDevice = onlyOnDevice; + + if (updatedStorageManager != null && !updatedStorageManager.equals(mStorageManager)) { + mStorageManager = updatedStorageManager; + ocFileListDelegate.setShowShareAvatar(mStorageManager + .getCapability(user.getAccountName()) + .getVersion() + .isShareesOnDavSupported()); + this.user = account; + } + + if (mStorageManager != null) { + + List allFiles = mStorageManager.getFolderContent(directory, onlyOnDevice); + mFiles.clear(); + + for (int i = 0; i < allFiles.size(); i++) { + OCFile ocFile = allFiles.get(i); + + //if e2ee folder has to hide then ignore if OCFile is encrypted + if (hideEncryptedFolder && ocFile.isEncrypted()) { + continue; + } + + if (ocFile.getMimeType().equals(MimeType.DIRECTORY)) { + mFiles.add(allFiles.get(i)); + } + } + + if (!preferences.isShowHiddenFilesEnabled()) { + mFiles = filterHiddenFiles(mFiles); + } + if (!limitToMimeType.isEmpty()) { + mFiles = filterByMimeType(mFiles, limitToMimeType); + } + FileSortOrder sortOrder = preferences.getSortOrderByFolder(directory); + mFiles = sortOrder.sortCloudFiles(mFiles); + mFilesAll.clear(); + mFilesAll.addAll(mFiles); + + currentDirectory = directory; + } else { + mFiles.clear(); + mFilesAll.clear(); + } + + new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged); + } + public void setData(List objects, SearchType searchType, FileDataStorageManager storageManager, diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java index 97a9b7460e82..714117c6f7d9 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java @@ -28,6 +28,11 @@ public interface OCFileListBottomSheetActions { */ void uploadFiles(); + /** + * offers a file scanner + */ + void scanDocument(); + /** * opens template selection for documents */ diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java index fd1a98f89655..229c0af92290 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java @@ -29,6 +29,7 @@ import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.ThemeUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; +import com.nmc.android.utils.ScanBotSdkUtils; /** * FAB menu {@link android.app.Dialog} styled as a bottom sheet for main actions. @@ -133,6 +134,13 @@ protected void onCreate(Bundle savedInstanceState) { if (!deviceInfo.hasCamera(getContext())) { binding.menuDirectCameraUpload.setVisibility(View.GONE); + binding.menuScanDocument.setVisibility(View.GONE); + } + + //check if scanbot sdk licence is valid or not + //hide the view if license is not valid + if(!ScanBotSdkUtils.isScanBotLicenseValid(fileActivity)){ + // binding.menuScanDocument.setVisibility(View.GONE); } // create rich workspace @@ -188,6 +196,11 @@ private void setupClickListener() { binding.menuScanDocUpload.setVisibility(View.GONE); } + binding.menuScanDocument.setOnClickListener(v -> { + actions.scanDocument(); + dismiss(); + }); + binding.menuUploadFiles.setOnClickListener(v -> { actions.uploadFiles(); dismiss(); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 4bd1e91c379c..3650c40d9d52 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 @@ -57,6 +57,7 @@ import com.nextcloud.utils.ShortcutUtil; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; +import com.nmc.android.marketTracking.TrackingScanInterface; import com.nextcloud.utils.view.FastScrollUtils; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -76,6 +77,7 @@ import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation; import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; +import com.nmc.android.ui.ScanActivity; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; @@ -228,6 +230,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, @@ -235,6 +244,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<>(); @@ -575,6 +586,24 @@ public void createRichWorkspace() { }).start(); } + @Override + public void scanDocument() { + //remote path to store the scans in the selected path + String remotePath = ""; + if (getActivity() != null && ((FileActivity) getActivity()).getCurrentDir() != null){ + remotePath = ((FileActivity) getActivity()).getCurrentDir().getRemotePath(); + } + + //remote path used so that user can directly save at the selected sub folder location + ScanActivity.openScanActivity(getActivity(), remotePath, FileDisplayActivity.REQUEST_CODE__SCAN_DOCUMENT); + + //track event on Scan Document button click + //implementation and logic will be available in nmc/1925-market_tracking + if (trackingScanInterface != null) { + trackingScanInterface.sendScanEvent(preferences); + } + } + @Override public void onShareIconClick(OCFile file) { if (file.isFolder()) { @@ -1281,6 +1310,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; @@ -1324,13 +1359,24 @@ public void listDirectory(OCFile directory, OCFile file, boolean onlyOnDevice, b } } - mAdapter.swapDirectory( - accountManager.getUser(), - directory, - storageManager, - onlyOnDevice, - mLimitToMimeType - ); + if(mShowOnlyFolder) { + mAdapter.showOnlyFolder( + accountManager.getUser(), + directory, + storageManager, + onlyOnDevice, + mLimitToMimeType, + mHideEncryptedFolder); + } + else { + mAdapter.swapDirectory( + accountManager.getUser(), + directory, + storageManager, + onlyOnDevice, + mLimitToMimeType + ); + } OCFile previousDirectory = mFile; mFile = directory; diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 6d8efcea33bd..810bf917ffa1 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 @@ -978,6 +978,23 @@ public void createFolder(String remotePath) { fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); } + /** + * create folder in remote if it doesn't exist if it exist then we will ignore creating + * + * @param remotePath to be created + * @param isCheckOnlyFolderExistence to check only folder exists or not and will not create folder if flag is true + */ + public void createFolderIfNotExist(String remotePath, boolean isCheckOnlyFolderExistence) { + Intent service = new Intent(fileActivity, OperationsService.class); + service.setAction(OperationsService.ACTION_CREATE_FOLDER_NOT_EXIST); + service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); + service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath); + service.putExtra(OperationsService.EXTRA_CHECK_ONLY_FOLDER_EXISTENCE, isCheckOnlyFolderExistence); + mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); + + fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); + } + /** * Cancel the transference in downloads (files/folders) and file uploads * diff --git a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java index ee4de5f4a9b4..f13b8a00b734 100644 --- a/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java +++ b/app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java @@ -33,6 +33,7 @@ public final class NotificationUtils { public static final String NOTIFICATION_CHANNEL_FILE_SYNC = "NOTIFICATION_CHANNEL_FILE_SYNC"; public static final String NOTIFICATION_CHANNEL_FILE_OBSERVER = "NOTIFICATION_CHANNEL_FILE_OBSERVER"; public static final String NOTIFICATION_CHANNEL_PUSH = "NOTIFICATION_CHANNEL_PUSH"; + public static final String NOTIFICATION_CHANNEL_IMAGE_SAVE = "NOTIFICATION_CHANNEL_IMAGE_SAVE"; private NotificationUtils() { // utility class -> private constructor diff --git a/app/src/main/java/com/owncloud/android/utils/StringUtils.java b/app/src/main/java/com/owncloud/android/utils/StringUtils.java index 1186619a871e..aa9594758ae9 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 5818cb3b89c8..016b2878e4e4 100644 --- a/app/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml +++ b/app/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml @@ -136,18 +136,50 @@ - + + + + + + + + + - - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_scanned_document.xml b/app/src/main/res/layout/fragment_edit_scanned_document.xml new file mode 100644 index 000000000000..44363cf2d671 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_scanned_document.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_scan_document.xml b/app/src/main/res/layout/fragment_scan_document.xml new file mode 100644 index 000000000000..a45c05ec53b3 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_document.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_scan_save.xml b/app/src/main/res/layout/fragment_scan_save.xml new file mode 100644 index 000000000000..4b5c01b06ae2 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_save.xml @@ -0,0 +1,429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_scanned_doc.xml b/app/src/main/res/layout/item_scanned_doc.xml new file mode 100644 index 000000000000..1d79e69e600d --- /dev/null +++ b/app/src/main/res/layout/item_scanned_doc.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_standard.xml b/app/src/main/res/layout/toolbar_standard.xml index c499ef9a97fe..eefa5e506952 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_scans.xml b/app/src/main/res/values-de/nmc_scans.xml new file mode 100644 index 000000000000..22461c5c3c5b --- /dev/null +++ b/app/src/main/res/values-de/nmc_scans.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 5761430919e7..5e4dd9f8cf8f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -113,6 +113,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! @@ -142,6 +143,7 @@ Löschen Umbenennen Speichern + Auswählen Senden Teilen Überspringen @@ -988,6 +990,7 @@ Herunterladen Video Überlagerungsicon Bitte warten… + Bitte geben Sie unter Apps & Benachrichtigungen in den Einstellungen manuell die Erlaubnis. Überprüfe gespeicherte Anmeldeinformationen Kopiere Datei von privatem Speicher Bitte aktualisieren Sie die Android System WebView-App für eine Anmeldung diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index e9c749b86943..76e4e05ca495 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -36,4 +36,68 @@ #1E1E1E @android:color/white + + + #FFFFFF + @color/grey_30 + @color/grey_30 + #CCCCCC + @color/grey_70 + @color/grey_80 + #2D2D2D + @color/grey_70 + @color/grey_70 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_60 + @color/grey_0 + @color/grey_0 + @color/grey_30 + #FFFFFF + @color/grey_30 + @color/grey_80 + #FFFFFF + + + @color/grey_80 + @color/grey_30 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + @color/grey_80 + + + @color/grey_70 + @color/grey_60 + + + @color/grey_70 + @color/grey_70 + + + #FFFFFF + @color/grey_30 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_60 + @color/grey_0 + #FFFFFF + + + #121212 + @color/grey_0 + @color/grey_80 + @color/grey_80 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7317a84ee0e1..55cbce4d0dd1 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -46,4 +46,13 @@ + + + @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 + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9a721eb3e385..248aa18d524c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -76,4 +76,93 @@ @android:color/white #666666 #A5A5A5 + + + #191919 + @color/primary + #191919 + #191919 + @color/grey_30 + @android:color/white + #FFFFFF + @color/grey_0 + #CCCCCC + #77c4ff + #B3FFFFFF + @color/grey_10 + + + #101010 + #F2F2F2 + #E5E5E5 + #B2B2B2 + #666666 + #4C4C4C + #333333 + + + @color/design_snackbar_background_color + @color/white + + + #FFFFFF + #191919 + + + @color/grey_0 + #191919 + @color/primary + #191919 + @color/primary + @color/grey_30 + @color/white + #191919 + + + #FFFFFF + #191919 + #191919 + + + #FFFFFF + #191919 + #FFFFFF + + + @color/primary + #F399C7 + #FFFFFF + @color/grey_30 + @color/grey_10 + @color/grey_0 + + + @color/primary + @color/grey_30 + @color/grey_30 + #CCCCCC + + + #191919 + @color/grey_30 + #191919 + #191919 + #191919 + #191919 + @color/grey_30 + #191919 + #000000 + #191919 + #F6E5EB + #C16F81 + #0D39DF + #0099ff + + + @color/grey_0 + #191919 + @color/grey_0 + @color/grey_30 + #77b6bb + #5077b6bb diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..cc9e25255a10 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,31 @@ + + + 4dp + 16dp + 24dp + 6dp + 18sp + 15sp + 15dp + 56dp + 86dp + 80dp + 11sp + 30dp + 55dp + 258dp + 17sp + 20dp + 160dp + 50dp + 150dp + 55dp + 48dp + 48dp + 24dp + 26dp + 20sp + 145dp + 1dp + 13sp + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_scans.xml b/app/src/main/res/values/nmc_scans.xml new file mode 100644 index 000000000000..cffc108323e3 --- /dev/null +++ b/app/src/main/res/values/nmc_scans.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 a7ebed20a682..5ca96164be7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1067,6 +1067,7 @@ Link Name Delete Link Settings + Please navigate to App info in settings and give permission manually. Confirm Strict mode: no HTTP connection allowed! Destination filename @@ -1098,6 +1099,8 @@ Error creating file from template No app available for sending the selected files Tap on a page to zoom in + Choose location + Select Full access Media read-only Photos and videos diff --git a/build.gradle b/build.gradle index c4a9144bb143..6e79c89b6d92 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ buildscript { prismVersion = "2.0.0" roomVersion = "2.6.1" workRuntime = "2.9.0" + scanbotSdkVersion = "4.0.0" ciBuild = System.getenv("CI") == "true" shotTest = System.getenv("SHOT_TEST") == "true" @@ -47,6 +48,14 @@ subprojects { google() mavenCentral() maven { url "https://jitpack.io" } + + // Scanbot SDK maven repos: + maven { + url 'https://nexus.scanbot.io/nexus/content/repositories/releases/' + } + maven { + url 'https://nexus.scanbot.io/nexus/content/repositories/snapshots/' + } } }