diff --git a/build.gradle b/build.gradle index 8c2cc635bc..b7b88363f0 100644 --- a/build.gradle +++ b/build.gradle @@ -274,6 +274,8 @@ dependencies { implementation "com.github.cotechde.hwsecurity:hwsecurity-fido:$fidoVersion" implementation "com.github.cotechde.hwsecurity:hwsecurity-fido2:$fidoVersion" + implementation 'com.github.zynkware:Document-Scanning-Android-SDK:1.0.1' + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.11.0' spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.7' diff --git a/drawable_resources/ic_scan_document.svg b/drawable_resources/ic_scan_document.svg new file mode 100644 index 0000000000..5d8d876202 --- /dev/null +++ b/drawable_resources/ic_scan_document.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png b/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png index 7b351248b4..11fb3fb7b2 100644 Binary files a/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png and b/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png differ diff --git a/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java index d11c82cea2..b554918c46 100644 --- a/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java +++ b/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java @@ -334,6 +334,11 @@ public class DialogFragmentIT extends AbstractIT { } + @Override + public void scanDocUpload() { + + } + @Override public void showTemplate(Creator creator, String headline) { diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index fd15977a44..3a65dc9468 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -117,6 +117,11 @@ android:name="android.app.searchable" android:resource="@xml/users_and_groups_searchable" /> + diff --git a/src/main/java/com/owncloud/android/ui/activity/AppScanActivity.kt b/src/main/java/com/owncloud/android/ui/activity/AppScanActivity.kt new file mode 100644 index 0000000000..6d80172b13 --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/activity/AppScanActivity.kt @@ -0,0 +1,59 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.ui.activity + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import com.owncloud.android.R +import com.owncloud.android.utils.DisplayUtils +import com.zynksoftware.documentscanner.ScanActivity +import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel +import com.zynksoftware.documentscanner.model.ScannerResults + +class AppScanActivity : ScanActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + addFragmentContentLayout() + } + + override fun onError(error: DocumentScannerErrorModel) { + DisplayUtils.showSnackMessage(this, R.string.error_starting_scan_doc) + } + + override fun onSuccess(scannerResults: ScannerResults) { + val intent = Intent() + + intent.putExtra( + "file", + scannerResults.transformedImageFile?.absolutePath ?: scannerResults.croppedImageFile?.absolutePath + ) + + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onClose() { + finish() + } +} diff --git a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 6342d3bfe8..20ee39908d 100644 --- a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -192,6 +192,7 @@ public class FileDisplayActivity extends FileActivity public static final int REQUEST_CODE__MOVE_FILES = REQUEST_CODE__LAST_SHARED + 3; public static final int REQUEST_CODE__COPY_FILES = REQUEST_CODE__LAST_SHARED + 4; public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5; + public static final int REQUEST_CODE__UPLOAD_SCAN_DOC_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 6; protected static final long DELAY_TO_REQUEST_REFRESH_OPERATION_LATER = DELAY_TO_REQUEST_OPERATIONS_LATER + 350; @@ -916,6 +917,35 @@ public class FileDisplayActivity extends FileActivity } } }, new String[]{FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath()}).execute(); + } else if (requestCode == REQUEST_CODE__UPLOAD_SCAN_DOC_FROM_CAMERA && + (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE)) { + Uri fileUri = Uri.parse(data.getStringExtra("file")); + + new CheckAvailableSpaceTask(new CheckAvailableSpaceTask.CheckAvailableSpaceListener() { + @Override + public void onCheckAvailableSpaceStart() { + Log_OC.d(this, "onCheckAvailableSpaceStart"); + } + + @Override + public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String... filesToUpload) { + Log_OC.d(this, "onCheckAvailableSpaceFinish"); + + if (hasEnoughSpaceAvailable) { + File file = new File(filesToUpload[0]); + File renamedFile = new File(file.getParent() + PATH_SEPARATOR + FileOperationsHelper.getCapturedImageName()); + + if (!file.renameTo(renamedFile)) { + DisplayUtils.showSnackMessage(getActivity(), "Fail to upload taken image!"); + return; + } + + requestUploadOfFilesFromFileSystem(renamedFile.getParentFile().getAbsolutePath(), + new String[]{renamedFile.getAbsolutePath()}, + FileUploader.LOCAL_BEHAVIOUR_DELETE); + } + } + }, new String[]{fileUri.getPath()}).execute(); } else if (requestCode == REQUEST_CODE__MOVE_FILES && resultCode == RESULT_OK) { exitSelectionMode(); final Intent fData = data; diff --git a/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java b/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java index 9b28bbf227..50766642d1 100644 --- a/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -499,7 +499,8 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList // return the list of files (success) Intent data = new Intent(); - if (requestCode == FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA) { + if (requestCode == FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA || + requestCode == FileDisplayActivity.REQUEST_CODE__UPLOAD_SCAN_DOC_FROM_CAMERA) { data.putExtra(EXTRA_CHOSEN_FILES, new String[]{filesToUpload[0]}); setResult(RESULT_OK_AND_DELETE, data); diff --git a/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java b/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java index 769a4c6cca..97b3f9f327 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java +++ b/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java @@ -62,6 +62,11 @@ public interface OCFileListBottomSheetActions { */ void directCameraUpload(); + /** + * offers scanning document upload to the current folder. + */ + void scanDocUpload(); + /** * open template selection for creator @link Creator */ diff --git a/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java b/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java index a070ac699c..84be01db91 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java +++ b/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java @@ -183,6 +183,11 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog { dismiss(); }); + binding.menuScanDocUpload.setOnClickListener(v -> { + actions.scanDocUpload(); + dismiss(); + }); + binding.menuUploadFiles.setOnClickListener(v -> { actions.uploadFiles(); dismiss(); diff --git a/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index e82810b122..917bcb9574 100644 --- a/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -42,6 +42,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.PopupMenu; +import android.widget.Toast; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.behavior.HideBottomViewOnScrollBehavior; @@ -478,6 +479,21 @@ public class OCFileListFragment extends ExtendedListFragment implements } } + @Override + public void scanDocUpload() { + FileDisplayActivity fileDisplayActivity = (FileDisplayActivity) getActivity(); + + if (fileDisplayActivity != null) { + fileDisplayActivity.getFileOperationsHelper() + .scanFromCamera(fileDisplayActivity, FileDisplayActivity.REQUEST_CODE__UPLOAD_SCAN_DOC_FROM_CAMERA); + } else { + Toast.makeText(getContext(), + getString(R.string.error_starting_direct_camera_upload), + Toast.LENGTH_SHORT) + .show(); + } + } + @Override public void uploadFiles() { UploadFilesActivity.startUploadActivityForResult( diff --git a/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index c59926c7fa..dda5b44447 100755 --- a/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -37,6 +37,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -68,6 +69,7 @@ import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.services.OperationsService; +import com.owncloud.android.ui.activity.AppScanActivity; import com.owncloud.android.ui.activity.ConflictsResolveActivity; import com.owncloud.android.ui.activity.ExternalSiteWebView; import com.owncloud.android.ui.activity.FileActivity; @@ -84,6 +86,7 @@ import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.PermissionUtil; import com.owncloud.android.utils.UriUtils; +import com.zynksoftware.documentscanner.ui.DocumentScanner; import org.greenrobot.eventbus.EventBus; @@ -92,7 +95,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -123,9 +126,9 @@ public class FileOperationsHelper { private static final String FILE_EXTENSION_WEBLOC = "webloc"; public static final int SINGLE_LINK_SIZE = 1; - private FileActivity fileActivity; - private CurrentAccountProvider currentAccount; - private ConnectivityService connectivityService; + private final FileActivity fileActivity; + private final CurrentAccountProvider currentAccount; + private final ConnectivityService connectivityService; /// Identifier of operation in progress which result shouldn't be lost private long mWaitingForOpId = Long.MAX_VALUE; @@ -145,7 +148,7 @@ public class FileOperationsHelper { InputStreamReader fr = null; BufferedReader br = null; try { - fr = new InputStreamReader(new FileInputStream(storagePath), Charset.forName("UTF-8")); + fr = new InputStreamReader(new FileInputStream(storagePath), StandardCharsets.UTF_8); br = new BufferedReader(fr); String line; @@ -1072,6 +1075,19 @@ public class FileOperationsHelper { } } + public void scanFromCamera(Activity activity, int requestCode) { + DocumentScanner.Configuration configuration = new DocumentScanner.Configuration(); + configuration.setImageType(Bitmap.CompressFormat.PNG); + DocumentScanner.INSTANCE.init(activity, configuration); + + Intent scanIntent = new Intent(activity, AppScanActivity.class); + if (PermissionUtil.checkSelfPermission(activity, Manifest.permission.CAMERA)) { + activity.startActivityForResult(scanIntent, requestCode); + } else { + PermissionUtil.requestCameraPermission(activity); + } + } + public static File createImageFile(Activity activity) { File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); diff --git a/src/main/res/drawable/ic_scan_document.xml b/src/main/res/drawable/ic_scan_document.xml new file mode 100644 index 0000000000..780fa2153f --- /dev/null +++ b/src/main/res/drawable/ic_scan_document.xml @@ -0,0 +1,13 @@ + + + diff --git a/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml b/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml index d11602e4e8..06440c2e51 100644 --- a/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml +++ b/src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml @@ -125,6 +125,36 @@ + + + + + + + + android:background="@color/list_divider_background" /> Add to %1$s Upload files Upload from camera + Scan document from camera Upload content from other apps Create new folder Virus detected. Upload cannot be completed! @@ -803,6 +804,7 @@ Close Failed to load details Error starting camera + Error using document scanning source folder is read-only; file will only be uploaded kept in original folder, as it is readonly Login via QR code