diff --git a/README.md b/README.md
index 3a2ed69..a60e1c1 100644
--- a/README.md
+++ b/README.md
@@ -42,10 +42,6 @@ Requirements
- Due to the version of OpenCV that is used, this project requires Android 5.0 (lollipop) or newer to run.
-- In order to capture and manipulate images, Open Note Scanner depends on having the OpenCV Manager application installed.
- - If not installed, Open Note Scanner will ask to download it from https://github.com/ctodobom/OpenCV-3.1.0-Android or from the Google Play Store.
-
-
How to Install
--------------
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ca30cb6..011fbff 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,22 +4,32 @@ plugins {
}
android {
- compileSdk = 33
- buildToolsVersion = "33.0.0"
+ compileSdk = 36
namespace = "com.todobom.opennotescanner"
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+
+ buildFeatures {
+ buildConfig = true
+ }
+ splits {
+ abi {
+ isEnable = true
+ reset()
+ include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
+ isUniversalApk = true
+ }
}
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11.toString()
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
}
defaultConfig {
applicationId = "com.todobom.opennotescanner"
minSdk = 21
- targetSdk = 31
- versionCode = 36
- versionName = "1.0.36"
+ targetSdk = 36
+ versionCode = 37
+ versionNameSuffix = "alpha"
+ versionName = "1.0.37"
}
buildTypes {
getByName("release") {
@@ -45,28 +55,36 @@ android {
}
}
+kotlin {
+ compilerOptions {
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
+ }
+}
+
dependencies {
- implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
- implementation("androidx.core:core-ktx:1.9.0")
- implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10")
+ implementation("androidx.core:core-ktx:1.16.0")
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.20")
- implementation("androidx.exifinterface:exifinterface:1.3.6")
+ implementation("com.guolindev.permissionx:permissionx:1.8.1")
+ implementation("androidx.exifinterface:exifinterface:1.4.1")
testImplementation("junit:junit:4.13.2")
- implementation("androidx.appcompat:appcompat:1.6.1")
- implementation("androidx.fragment:fragment-ktx:1.5.5")
- implementation("com.google.android.material:material:1.8.0")
- implementation("com.google.zxing:core:3.5.1")
- implementation("com.github.ctodobom:OpenCV-3.1.0-Android:9e00ee4218ca0c9e60a905c9f09bf499f9dc5115")
- implementation("us.feras.mdv:markdownview:1.1.0")
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("androidx.fragment:fragment-ktx:1.8.8")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("com.google.zxing:core:3.5.3")
+ implementation("org.opencv:opencv:4.12.0")
+ implementation("io.noties.markwon:core:4.6.2")
+ implementation("io.noties.markwon:html:4.6.2")
implementation("com.github.ctodobom:drag-select-recyclerview:0.3.4.ctodobom.sections")
implementation("com.github.allgood:Android-Universal-Image-Loader:717a00c")
implementation("com.github.ctodobom:FabToolbar:3c5f0e0ff1b6d5089e20b7da7157a604075ae943")
- implementation("com.github.matomo-org:matomo-sdk-android:4.1.4")
- implementation("com.github.MikeOrtiz:TouchImageView:3.3")
+ implementation("com.github.matomo-org:matomo-sdk-android:4.3.4")
+ implementation("com.github.MikeOrtiz:TouchImageView:3.7.1")
- val itextpdf_version = "7.2.5"
- implementation("com.itextpdf:kernel:$itextpdf_version")
- implementation("com.itextpdf:layout:$itextpdf_version")
- implementation("com.itextpdf:io:$itextpdf_version")
+ val itextpdfVersion = "9.2.0"
+ implementation("com.itextpdf:kernel:$itextpdfVersion")
+ implementation("com.itextpdf:layout:$itextpdfVersion")
+ implementation("com.itextpdf:io:$itextpdfVersion")
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5c4f43e..5fbc920 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,13 +3,15 @@
+ android:required="true" />
-
-
+
+
+
+
diff --git a/app/src/main/java/com/todobom/opennotescanner/FullScreenImageAdapter.kt b/app/src/main/java/com/todobom/opennotescanner/FullScreenImageAdapter.kt
index 069261d..1139da5 100644
--- a/app/src/main/java/com/todobom/opennotescanner/FullScreenImageAdapter.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/FullScreenImageAdapter.kt
@@ -2,6 +2,7 @@ package com.todobom.opennotescanner
import android.app.Activity
import android.content.Context
+import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -17,13 +18,13 @@ import java.util.*
*/
class FullScreenImageAdapter(
private val _activity: Activity,
- private val _imagePaths: ArrayList
+ private val _imageUris: ArrayList
) : PagerAdapter() {
private var maxTexture = 0
private var mImageLoader: ImageLoader? = null
private var mTargetSize: ImageSize? = null
override fun getCount(): Int {
- return _imagePaths.size
+ return _imageUris.size
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
@@ -37,7 +38,7 @@ class FullScreenImageAdapter(
val viewLayout = inflater.inflate(R.layout.layout_fullscreen_image, container,
false)
imgDisplay = viewLayout.findViewById(R.id.imgDisplay) as TouchImageView
- val imagePath = _imagePaths[position]
+ val imageUri = _imageUris[position]
/*
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
@@ -62,13 +63,13 @@ class FullScreenImageAdapter(
*/
// imgDisplay.setImageBitmap(bitmap);
- mImageLoader!!.displayImage("file:///$imagePath", imgDisplay, mTargetSize)
+ mImageLoader!!.displayImage(imageUri.toString(), imgDisplay, mTargetSize)
container.addView(viewLayout)
return viewLayout
}
- fun getPath(position: Int): String {
- return _imagePaths[position]
+ fun getUri(position: Int): Uri {
+ return _imageUris[position]
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
diff --git a/app/src/main/java/com/todobom/opennotescanner/FullScreenViewActivity.kt b/app/src/main/java/com/todobom/opennotescanner/FullScreenViewActivity.kt
index 600d09c..631d44f 100644
--- a/app/src/main/java/com/todobom/opennotescanner/FullScreenViewActivity.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/FullScreenViewActivity.kt
@@ -1,15 +1,19 @@
package com.todobom.opennotescanner
import android.app.AlertDialog
+import android.content.Context
import android.content.DialogInterface
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import android.webkit.MimeTypeMap
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
+import androidx.core.net.toFile
import androidx.viewpager.widget.ViewPager
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
import com.nostra13.universalimageloader.core.ImageLoader
@@ -20,7 +24,6 @@ import com.todobom.opennotescanner.helpers.Utils
import com.todobom.opennotescanner.helpers.Utils.Companion.maxTextureSize
import com.todobom.opennotescanner.helpers.Utils.Companion.removeImageFromGallery
import com.todobom.opennotescanner.views.TagEditorFragment
-import java.io.File
class FullScreenViewActivity : AppCompatActivity() {
private lateinit var utils: Utils
@@ -83,7 +86,7 @@ class FullScreenViewActivity : AppCompatActivity() {
private fun loadAdapter(): FullScreenImageAdapter {
mViewPager.adapter = null
- val adapter = FullScreenImageAdapter(this@FullScreenViewActivity, utils.filePaths)
+ val adapter = FullScreenImageAdapter(this@FullScreenViewActivity, utils.fileUris)
adapter.setImageLoader(mImageLoader)
adapter.setMaxTexture(mMaxTexture, mTargetSize)
mViewPager.adapter = adapter
@@ -127,10 +130,20 @@ class FullScreenViewActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item)
}
+ private fun isPng(context: Context, uri: Uri): Boolean {
+ // First try MIME type
+ val type = context.contentResolver.getType(uri)
+ if (type == "image/png") return true
+
+ // Fallback: try extension
+ val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
+ return extension.equals("png", ignoreCase = true)
+ }
+
private fun tagImage() {
val item = mViewPager.currentItem
- val filePath = mAdapter.getPath(item)
- if (filePath.endsWith(".png")) {
+ val fileUri = mAdapter.getUri(item)
+ if (isPng(this, fileUri)) {
val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.format_not_supported)
builder.setMessage(R.string.format_not_supported_message)
@@ -139,32 +152,30 @@ class FullScreenViewActivity : AppCompatActivity() {
alerta.show()
return
}
- val fm = supportFragmentManager
- val tagEditorDialog = TagEditorFragment()
- tagEditorDialog.setFilePath(filePath)
- tagEditorDialog.setRunOnDetach { }
- tagEditorDialog.show(fm, "tageditor_view")
+ val tagEditorDialog = TagEditorFragment(fileUri)
+ tagEditorDialog.show(supportFragmentManager, "tageditor_view")
}
private fun deleteImage() {
val item = mViewPager.currentItem
- val filePath = mAdapter.getPath(item)
- val photoFile = File(filePath)
- photoFile.delete()
- removeImageFromGallery(filePath, this)
+ val fileUri = mAdapter.getUri(item)
+ removeImageFromGallery(fileUri, this)
loadAdapter()
if (0 == mAdapter.count) finish()
mViewPager.currentItem = item
}
fun shareImage() {
- val pager = mViewPager
- val item = pager.currentItem
+ val uri = mAdapter.getUri(mViewPager.currentItem)
val shareIntent = Intent(Intent.ACTION_SEND)
- shareIntent.type = "image/jpg"
- val uri = FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", File(mAdapter.getPath(item)))
- shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
- Log.d("Fullscreen", "uri $uri")
+ val shareUri = if (uri.scheme == "file") {
+ FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", uri.toFile())
+ } else {
+ uri
+ }
+ shareIntent.type = this.contentResolver.getType(shareUri)
+ shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri)
+ Log.d("Fullscreen", "uri $shareUri")
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_snackbar)))
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/todobom/opennotescanner/GalleryGridActivity.kt b/app/src/main/java/com/todobom/opennotescanner/GalleryGridActivity.kt
index b9479b4..0fec4b6 100644
--- a/app/src/main/java/com/todobom/opennotescanner/GalleryGridActivity.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/GalleryGridActivity.kt
@@ -16,6 +16,7 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
+import androidx.core.net.toFile
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.dragselectrecyclerview.DragSelectRecyclerView
@@ -77,11 +78,10 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
setSelectionMode(i > 0)
}
- inner class ThumbAdapter(activity: GalleryGridActivity?, files: ArrayList) : DragSelectRecyclerViewAdapter() {
- private val mCallback: ClickListener?
- var itemList = ArrayList()
- fun add(path: String) {
- itemList.add(path)
+ inner class ThumbAdapter(val activity: GalleryGridActivity?, val fileUris: ArrayList) : DragSelectRecyclerViewAdapter() {
+
+ init {
+ setSelectionListener(activity)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbViewHolder {
@@ -91,17 +91,17 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
override fun onBindViewHolder(holder: ThumbViewHolder, position: Int) {
super.onBindViewHolder(holder, position) // this line is important!
- val filename = itemList[position]
- if (filename != holder.filename) {
+ val fileUri = fileUris[position]
+ if (fileUri != holder.fileUri) {
// remove previous image
holder.image.setImageBitmap(null)
// Load image, decode it to Bitmap and return Bitmap to callback
- mImageLoader.displayImage("file:///$filename", holder.image, mTargetSize)
+ mImageLoader.displayImage(fileUri.toString(), holder.image, mTargetSize)
// holder.image.setImageBitmap(decodeSampledBitmapFromUri(filename, 220, 220));
- holder.filename = filename
+ holder.fileUri = fileUri
}
if (isIndexSelected(position)) {
holder.image.setColorFilter(Color.argb(140, 0, 255, 0))
@@ -111,29 +111,29 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
}
override fun getItemCount(): Int {
- return itemList.size
+ return fileUris.size
}
- val selectedFiles: ArrayList
+ val selectedFiles: ArrayList
get() {
- val selection = ArrayList()
+ val selection = ArrayList()
for (i in selectedIndices) {
- selection.add(itemList[i!!])
+ selection.add(fileUris[i])
}
return selection
}
inner class ThumbViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener, OnLongClickListener {
val image: ImageView
- var filename: String? = null
+ var fileUri: Uri? = null
override fun onClick(v: View) {
// Forwards to the adapter's constructor callback
- mCallback?.onClick(adapterPosition)
+ activity?.onClick(adapterPosition)
}
override fun onLongClick(v: View): Boolean {
// Forwards to the adapter's constructor callback
- mCallback?.onLongClick(adapterPosition)
+ activity?.onLongClick(adapterPosition)
return true
}
@@ -145,18 +145,10 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
this.itemView.setOnLongClickListener(this)
}
}
-
- // Constructor takes click listener callback
- init {
- mCallback = activity
- for (file in files) {
- add(file)
- }
- setSelectionListener(activity)
- }
}
var myThumbAdapter: ThumbAdapter? = null
+
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mSharedPref = PreferenceManager.getDefaultSharedPreferences(this)
@@ -170,7 +162,7 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
mImageLoader = ImageLoader.getInstance()
mImageLoader.init(config)
mTargetSize = ImageSize(220, 220) // result Bitmap will be fit to this size
- val ab = ArrayList()
+ val ab = ArrayList()
myThumbAdapter = ThumbAdapter(this, ab)
// new Utils(getApplicationContext()).getFilePaths(););
recyclerView = findViewById(R.id.recyclerview) as DragSelectRecyclerView
@@ -189,8 +181,7 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
private fun reloadAdapter() {
recyclerView.setAdapter(null)
- // ArrayList ab = new ArrayList<>();
- myThumbAdapter = ThumbAdapter(this, Utils(applicationContext).filePaths)
+ myThumbAdapter = ThumbAdapter(this, Utils(applicationContext).fileUris)
recyclerView.setAdapter(myThumbAdapter)
recyclerView.invalidate()
setSelectionMode(false)
@@ -203,11 +194,8 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
private fun deleteImage() {
for (filePath in myThumbAdapter!!.selectedFiles) {
- val photoFile = File(filePath)
- if (photoFile.delete()) {
- removeImageFromGallery(filePath, this)
- Log.d(TAG, "Removed file: $filePath")
- }
+ removeImageFromGallery(filePath, this)
+ Log.d(TAG, "Removed file: $filePath")
}
reloadAdapter()
}
@@ -263,14 +251,24 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
}
fun pdfExport() {
- val pdfFilePath = mergeImagesToPdf(applicationContext, myThumbAdapter!!.selectedFiles)
- if (pdfFilePath != null) {
+ var pdfFileUri = mergeImagesToPdf(applicationContext, myThumbAdapter!!.selectedFiles)
+ if (pdfFileUri != null) {
+ if (pdfFileUri.scheme == "file") {
+ // if pdf was written pre Android Q we need a FileProvider to allow access
+ // if MediaStore was used, the Uri can be shared and permissions are set up already
+ // not a fan of this, maybe should revert usage of MediaStore
+ pdfFileUri = FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", File(pdfFileUri.path!!))
+ } else if (pdfFileUri.scheme != "content") {
+ // unrecognized type
+ Log.e(TAG, "Unsupported URI scheme in PDF Export: ${pdfFileUri.scheme}")
+ return
+ }
+
try {
- val file = File(pdfFilePath)
- val i = Intent(Intent.ACTION_VIEW, FileProvider.getUriForFile(applicationContext,
- "$packageName.fileprovider", file))
- i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- startActivity(i)
+ startActivity(Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(pdfFileUri, "application/pdf")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ })
} catch (e: ActivityNotFoundException) {
Toast.makeText(applicationContext, "Cant Find Your File", Toast.LENGTH_LONG).show()
}
@@ -282,20 +280,28 @@ class GalleryGridActivity : AppCompatActivity(), ClickListener, DragSelectRecycl
if (selectedFiles.size == 1) {
/* Only one scanned document selected: ACTION_SEND intent */
val shareIntent = Intent(Intent.ACTION_SEND)
- shareIntent.type = "image/jpg"
- val uri = FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", File(selectedFiles[0]))
- shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
- Log.d("GalleryGridActivity", "uri $uri")
+ val shareUri = if (selectedFiles[0].scheme == "file") {
+ FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", selectedFiles[0].toFile())
+ } else {
+ selectedFiles[0]
+ }
+ shareIntent.type = this.contentResolver.getType(shareUri)
+ shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri)
+ Log.d("GalleryGridActivity", "uri $shareUri")
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_snackbar)))
} else {
val filesUris = ArrayList()
- for (i in myThumbAdapter!!.selectedFiles) {
- val uri = FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", File(i))
- filesUris.add(uri)
- Log.d("GalleryGridActivity", "uri $uri")
+ for (selectedUri in myThumbAdapter!!.selectedFiles) {
+ val shareUri = if (selectedFiles[0].scheme == "file") {
+ FileProvider.getUriForFile(applicationContext, "$packageName.fileprovider", selectedUri.toFile())
+ } else {
+ selectedUri
+ }
+ filesUris.add(shareUri)
+ Log.d("GalleryGridActivity", "uri $shareUri")
}
val shareIntent = Intent(Intent.ACTION_SEND_MULTIPLE)
- shareIntent.type = "image/jpg"
+ shareIntent.type = "image/jpg" // TODO: check mimetype
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, filesUris)
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_snackbar)))
}
diff --git a/app/src/main/java/com/todobom/opennotescanner/ImageProcessor.java b/app/src/main/java/com/todobom/opennotescanner/ImageProcessor.java
index 8a01fa2..ec9eb81 100644
--- a/app/src/main/java/com/todobom/opennotescanner/ImageProcessor.java
+++ b/app/src/main/java/com/todobom/opennotescanner/ImageProcessor.java
@@ -6,7 +6,6 @@
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.shapes.PathShape;
-import android.hardware.SensorManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -37,7 +36,6 @@
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
-import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import java.util.ArrayList;
@@ -77,8 +75,8 @@ public ImageProcessor(Looper looper, OpenNoteScannerActivity mainActivity) {
String docPageFormat = sharedPref.getString("document_page_format", "0");
mDocumentAspectRatio = 0;
if (docPageFormat.equals("0.0001")) {
- Float customPageWidth = Float.parseFloat(sharedPref.getString("custom_pageformat_width" , "0" ));
- Float customPageHeight = Float.parseFloat(sharedPref.getString("custom_pageformat_height" , "0" ));
+ float customPageWidth = Float.parseFloat(sharedPref.getString("custom_pageformat_width" , "0" ));
+ float customPageHeight = Float.parseFloat(sharedPref.getString("custom_pageformat_height" , "0" ));
if (customPageWidth > 0 && customPageHeight > 0) {
mDocumentAspectRatio = customPageHeight / customPageWidth;
}
@@ -95,7 +93,7 @@ public void handleMessage ( Message msg ) {
String command = obj.getCommand();
- Log.d(TAG, "Message Received: " + command + " - " + obj.getObj().toString() );
+ Log.v(TAG, "Message Received: " + command + " - " + obj.getObj().toString() );
if ( command.equals("previewFrame")) {
processPreviewFrame((PreviewFrame) obj.getObj());
@@ -123,7 +121,7 @@ private void processPreviewFrame( PreviewFrame previewFrame ) {
}
boolean qrOk = false;
- String currentQR=null;
+ String currentQR = null;
for (Result result: results) {
String qrText = result.getText();
@@ -140,10 +138,10 @@ private void processPreviewFrame( PreviewFrame previewFrame ) {
boolean autoMode = previewFrame.isAutoMode();
boolean previewOnly = previewFrame.isPreviewOnly();
+ // request picture if document is detected and either scan button is clicked and not in auto mode or qr code is detected in auto mode
+ // FIXME: consider simplifying this. isPreviewOnly contains isAutoMode, e.g if autoMode is true, isPreviewOnly will always be false
if ( detectPreviewDocument(frame) && ( (!autoMode && !previewOnly ) || ( autoMode && qrOk ) ) ) {
-
mMainActivity.waitSpinnerVisible();
-
mMainActivity.requestPicture();
if (qrOk) {
@@ -154,22 +152,17 @@ private void processPreviewFrame( PreviewFrame previewFrame ) {
frame.release();
mMainActivity.setImageProcessorBusy(false);
-
}
public void processPicture( Mat picture ) {
-
- Mat img = Imgcodecs.imdecode(picture, Imgcodecs.CV_LOAD_IMAGE_UNCHANGED);
- picture.release();
-
- Log.d(TAG, "processPicture - imported image " + img.size().width + "x" + img.size().height);
+ Log.d(TAG, "processPicture - imported image " + picture.size().width + "x" + picture.size().height);
if (mBugRotate) {
- Core.flip(img, img, 1 );
- Core.flip(img, img, 0 );
+ Core.flip(picture, picture, 1 );
+ Core.flip(picture, picture, 0 );
}
- ScannedDocument doc = detectDocument(img);
+ ScannedDocument doc = detectDocument(picture);
mMainActivity.saveDocument(doc);
doc.release();
@@ -252,7 +245,7 @@ private boolean detectPreviewDocument(Mat inputRgba) {
drawDocumentBox(mPreviewPoints, mPreviewSize);
- Log.d(TAG, quad.getPoints()[0].toString() + " , " + quad.getPoints()[1].toString() + " , " + quad.getPoints()[2].toString() + " , " + quad.getPoints()[3].toString());
+// Log.d(TAG, quad.getPoints()[0].toString() + " , " + quad.getPoints()[1].toString() + " , " + quad.getPoints()[2].toString() + " , " + quad.getPoints()[3].toString());
return true;
diff --git a/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerActivity.kt b/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerActivity.kt
index 7363744..0baf142 100644
--- a/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerActivity.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerActivity.kt
@@ -3,18 +3,19 @@ package com.todobom.opennotescanner
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
+import android.content.ContentValues
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
-import android.content.pm.PackageManager
import android.content.res.ColorStateList
+import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.hardware.*
import android.hardware.Camera.*
import android.media.AudioManager
-import android.media.MediaPlayer
+import android.media.MediaActionSound
import android.net.Uri
import android.os.*
import android.preference.PreferenceManager
@@ -26,22 +27,22 @@ import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
import androidx.exifinterface.media.ExifInterface
import com.github.fafaldo.fabtoolbar.widget.FABToolbarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
+import com.permissionx.guolindev.PermissionX
import com.todobom.opennotescanner.helpers.*
import com.todobom.opennotescanner.helpers.ScanTopicDialogFragment.SetTopicDialogListener
import com.todobom.opennotescanner.views.HUDCanvasView
import org.matomo.sdk.Tracker
import org.matomo.sdk.extra.TrackHelper
-import org.opencv.android.BaseLoaderCallback
import org.opencv.android.OpenCVLoader
import org.opencv.core.Core
import org.opencv.core.CvType
import org.opencv.core.Mat
+import org.opencv.core.MatOfByte
+import org.opencv.core.MatOfInt
import org.opencv.core.Size
import org.opencv.imgcodecs.Imgcodecs
import org.opencv.imgproc.Imgproc
@@ -78,7 +79,7 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
}
private var mVisible = false
private val mHideRunnable = Runnable { hide() }
- private var _shootMP: MediaPlayer? = null
+ private var mediaActionSound: MediaActionSound? = null
private var safeToTakePicture = false
private lateinit var scanDocButton: Button
private lateinit var mImageThread: HandlerThread
@@ -194,37 +195,46 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
}
fun setFlash(stateFlash: Boolean): Boolean {
- val pm = packageManager
val camera = mCamera ?: return false
- if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
- val par = camera.parameters
- par.flashMode = if (stateFlash) Camera.Parameters.FLASH_MODE_TORCH else Camera.Parameters.FLASH_MODE_OFF
- camera.parameters = par
+ val flashModes = camera.parameters.supportedFlashModes
+ if (flashModes != null && flashModes.contains(Camera.Parameters.FLASH_MODE_TORCH)) {
+ val parameters = camera.parameters
+ parameters.flashMode = if (stateFlash) Camera.Parameters.FLASH_MODE_TORCH else Camera.Parameters.FLASH_MODE_OFF
+ camera.parameters = parameters
Log.d(TAG, "flash: " + if (stateFlash) "on" else "off")
return stateFlash
}
+
+ Log.d(TAG, "flash not available")
return false
}
- private fun checkResumePermissions() {
- if (ContextCompat.checkSelfPermission(this,
- Manifest.permission.CAMERA)
- != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),
- RESUME_PERMISSIONS_REQUEST_CAMERA)
+ private fun grantPermissions() {
+ val permissionsToRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // new version will use MediaStore or SAF, does not need permissions
+ listOf(Manifest.permission.CAMERA)
} else {
- enableCameraView()
+ // TODO: can we move this to use SAF / MediaStore too ?
+ listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)
}
- }
- private fun checkCreatePermissions() {
- if (ContextCompat.checkSelfPermission(this,
- Manifest.permission.WRITE_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
- MY_PERMISSIONS_REQUEST_WRITE)
- }
+ PermissionX.init(this)
+ .permissions(permissionsToRequest)
+ .onExplainRequestReason { scope, deniedList ->
+ scope.showRequestReasonDialog(deniedList, getString(R.string.permission_explain_request_reason_all), getString(R.string.ok))
+ }
+ .onForwardToSettings { scope, deniedList ->
+ scope.showForwardToSettingsDialog(deniedList, getString(R.string.permission_forward_reason_all), getString(R.string.ok))
+ }
+ .explainReasonBeforeRequest()
+ .request { allGranted, _, deniedList ->
+ if (allGranted) {
+ enableCameraView()
+ } else {
+ // PermissionX will always prompt or redirect to settings if permissions are not granted.
+ }
+ }
}
fun turnCameraOn() {
@@ -243,29 +253,6 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
}
}
- @SuppressLint("MissingSuperCall")
- override fun onRequestPermissionsResult(requestCode: Int,
- permissions: Array, grantResults: IntArray) {
- when (requestCode) {
- CREATE_PERMISSIONS_REQUEST_CAMERA -> {
-
- // If request is cancelled, the result arrays are empty.
- if (grantResults.size > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- turnCameraOn()
- }
- }
- RESUME_PERMISSIONS_REQUEST_CAMERA -> {
-
- // If request is cancelled, the result arrays are empty.
- if (grantResults.size > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- enableCameraView()
- }
- }
- }
- }
-
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
@@ -313,23 +300,11 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
mHideHandler.postDelayed(mHideRunnable, delayMillis.toLong())
}
- private val mLoaderCallback: BaseLoaderCallback = object : BaseLoaderCallback(this) {
- override fun onManagerConnected(status: Int) {
- when (status) {
- SUCCESS -> {
- checkResumePermissions()
- }
- else -> {
- Log.d(TAG, "opencvstatus: $status")
- super.onManagerConnected(status)
- }
- }
- }
- }
-
public override fun onResume() {
super.onResume()
+ grantPermissions()
+
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also { accelerometer ->
sensorManager.registerListener(
this,
@@ -357,8 +332,15 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
for (build in Build.SUPPORTED_ABIS) {
Log.d(TAG, "myBuild $build")
}
- checkCreatePermissions()
- CustomOpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, mLoaderCallback)
+
+ if (OpenCVLoader.initLocal()) {
+ Log.i(TAG, "OpenCV loaded successfully");
+ } else {
+ Log.e(TAG, "OpenCV initialization failed!");
+ (Toast.makeText(this, "OpenCV initialization failed!", Toast.LENGTH_LONG)).show();
+ return;
+ }
+
//TODO these should go in the variable's creation
mImageThread = HandlerThread("Worker Thread")
mImageThread.start()
@@ -390,6 +372,9 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
// Don't receive any more updates from either sensor.
sensorManager.unregisterListener(this)
+
+ mediaActionSound?.release()
+ mediaActionSound = null
}
val resolutionList: List
@@ -467,30 +452,37 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
fun setFocusParameters() {
val camera = mCamera ?: return
- val param: Camera.Parameters = camera.parameters
- val pm = packageManager
- if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_AUTOFOCUS)) {
- try {
- camera.setAutoFocusMoveCallback { start, _ ->
- mFocused = !start
- Log.d(TAG, "focusMoving: $mFocused")
- }
- } catch (e: Exception) {
- Log.d(TAG, "failed setting AutoFocusMoveCallback")
- }
- param.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
+
+ val parameters = camera.parameters
+ val supportedFocusModes = camera.parameters.getSupportedFocusModes()
+ if (supportedFocusModes != null && supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
+ parameters.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
+ } else if (supportedFocusModes != null && supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ // fallback
+ parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
val targetFocusRect = Rect(-500, -500, 500, 500)
val focusList: MutableList = ArrayList()
val focusArea = Camera.Area(targetFocusRect, 1000)
focusList.add(focusArea)
- param.focusAreas = focusList
- param.meteringAreas = focusList
- camera.parameters = param
- Log.d(TAG, "enabling autofocus")
+ parameters.focusAreas = focusList
+ parameters.meteringAreas = focusList
} else {
mFocused = true
Log.d(TAG, "autofocus not available")
+ return
}
+
+ try {
+ camera.setAutoFocusMoveCallback { start, _ ->
+ mFocused = !start
+ Log.d(TAG, "focusMoving: $mFocused")
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "failed setting AutoFocusMoveCallback")
+ }
+
+ Log.d(TAG, "enabling autofocus")
+ camera.parameters = parameters
}
override fun surfaceCreated(holder: SurfaceHolder) {
@@ -502,8 +494,8 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
}
mCamera = camera
- val param: Camera.Parameters
- param = camera.getParameters()
+ val param = camera.getParameters()
+ param.pictureFormat = ImageFormat.JPEG
val pSize = maxPreviewResolution
param.setPreviewSize(pSize!!.width, pSize.height)
val previewRatio = pSize.width.toFloat() / pSize.height
@@ -539,7 +531,7 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
this.mDocumentAspectRatio = docPageFormat!!.toFloat().toDouble()
}
- var hotArea = Utils.getHotArea(pSize.width, pSize.height, this)
+ val hotArea = Utils.getHotArea(pSize.width, pSize.height, this)
hotAreaSpaceWidth = hotArea!![1]
hotAreaSpaceHeight = hotArea!![0]
@@ -569,10 +561,7 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
param.setPictureSize(maxRes.width, maxRes.height)
Log.d(TAG, "max supported picture resolution: " + maxRes.width + "x" + maxRes.height)
}
- val pm = packageManager
- if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
- param.flashMode = if (mFlashMode) Camera.Parameters.FLASH_MODE_TORCH else Camera.Parameters.FLASH_MODE_OFF
- }
+
camera.setParameters(param)
mBugRotate = mSharedPref.getBoolean("bug_rotate", false)
if (mBugRotate) {
@@ -619,7 +608,7 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
override fun onPreviewFrame(data: ByteArray, camera: Camera) {
val pictureSize = camera.parameters.previewSize
- Log.d(TAG, "onPreviewFrame - received image " + pictureSize.width + "x" + pictureSize.height
+ Log.v(TAG, "onPreviewFrame - received image " + pictureSize.width + "x" + pictureSize.height
+ " focused: " + mFocused + " imageprocessor: " + if (imageProcessorBusy) "busy" else "available")
if (mFocused && !imageProcessorBusy) {
setImageProcessorBusy(true)
@@ -649,7 +638,11 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
if (safeToTakePicture) {
runOnUiThread(resetShutterColor)
safeToTakePicture = false
- camera.takePicture(null, null, mThis)
+ try {
+ camera.takePicture(null, null, mThis)
+ } catch (_: java.lang.Exception) {
+ Log.e(TAG, "failed to take picture")
+ }
return true
}
return false
@@ -658,19 +651,41 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
override fun onPictureTaken(data: ByteArray, camera: Camera) {
shootSound()
setFocusParameters()
- val pictureSize = camera.parameters.pictureSize
- Log.d(TAG, "onPictureTaken - received image " + pictureSize.width + "x" + pictureSize.height)
- mat = Mat(Size(pictureSize.width.toDouble(), pictureSize.height.toDouble()), CvType.CV_8U).also {
- it.put(0, 0, data)
+ Log.d(TAG, "onPictureTaken - received ${data.size} bytes")
+
+ val encodedMat = MatOfByte(*data) // or MatOfByte(data)
+ val decodedMat: Mat
+ try {
+ decodedMat = Imgcodecs.imdecode(encodedMat, Imgcodecs.IMREAD_UNCHANGED)
+ if (decodedMat.empty()) {
+ Log.e(TAG, "Failed to decode image from data byte array.")
+ refreshCamera() // Or some other error recovery
+ safeToTakePicture = true
+ return
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception while decoding image: ${e.message}")
+ refreshCamera()
+ safeToTakePicture = true
+ return
+ } finally {
+ encodedMat.release() // Release the temporary MatOfByte
}
+ Log.d(TAG, "Decoded image: ${decodedMat.width()}x${decodedMat.height()}, type: ${CvType.typeToString(decodedMat.type())}")
+
+ // Store the decoded Mat to be used by issueProcessingOfTakenPicture
+ mat = decodedMat
+
if (mSharedPref.getBoolean("custom_scan_topic", false)) {
val fm = supportFragmentManager
val scanTopicDialogFragment = ScanTopicDialogFragment()
scanTopicDialogFragment.show(fm, getString(R.string.scan_topic_dialog_title))
- return
+ // Note: if custom_scan_topic is true, issueProcessingOfTakenPicture
+ // will be called later by onFinishTopicDialog. 'this.mat' must hold the decoded image.
+ } else {
+ issueProcessingOfTakenPicture()
}
- issueProcessingOfTakenPicture()
}
override fun onFinishTopicDialog(inputText: String?) {
@@ -686,7 +701,7 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
}
fun sendImageProcessorMessage(messageText: String, obj: Any?) {
- Log.d(TAG, "sending message to ImageProcessor: " + messageText + " - " + obj.toString())
+ Log.v(TAG, "sending message to ImageProcessor: $messageText - $obj")
val msg = mImageProcessor.obtainMessage()
msg.obj = OpenNoteMessage(messageText, obj)
mImageProcessor.sendMessage(msg)
@@ -694,119 +709,201 @@ class OpenNoteScannerActivity : AppCompatActivity(), NavigationView.OnNavigation
fun saveDocument(scannedDocument: ScannedDocument) {
val doc = scannedDocument.processed ?: scannedDocument.original
+
val intent = intent
- val fileName: String
- var isIntent = false
- var fileUri: Uri? = null
- var imgSuffix = ".jpg"
- if (mSharedPref.getBoolean("save_png", false)) {
- imgSuffix = ".png"
- }
- if (intent.action == "android.media.action.IMAGE_CAPTURE") {
- fileUri = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT) as Uri
- Log.d(TAG, "intent uri: $fileUri")
- fileName = try {
- File.createTempFile("onsFile", imgSuffix, this.cacheDir).path
- } catch (e: IOException) {
- e.printStackTrace()
- return
- }
- isIntent = true
+ val isIntentCapture = intent.action == "android.media.action.IMAGE_CAPTURE"
+ val outputUriFromIntent = if (isIntentCapture) {
+ intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT)
} else {
- val folderName = mSharedPref.getString("storage_folder", "OpenNoteScanner")
- val folder = File(Environment.getExternalStorageDirectory().toString(), "/$folderName")
- if (!folder.exists()) {
- folder.mkdirs()
- Log.d(TAG, "wrote: created folder " + folder.path)
- }
- fileName = createFileName(imgSuffix, folderName)
+ null
+ }
+
+ val imageSuffix = if (mSharedPref.getBoolean("save_png", false)) ".png" else ".jpg"
+ val mimeType = if (imageSuffix == ".png") "image/png" else "image/jpeg"
+
+ val encodingParams = MatOfInt()
+ if (imageSuffix == ".jpg") {
+ encodingParams.fromArray(Imgcodecs.IMWRITE_JPEG_QUALITY, mSharedPref.getInt("jpeg_quality", 95)) // Example: get quality from prefs
+ } else {
+ encodingParams.fromArray(Imgcodecs.IMWRITE_PNG_COMPRESSION, mSharedPref.getInt("png_compression", 6)) // Example: get compression from prefs
}
- val endDoc = Mat(java.lang.Double.valueOf(doc.size().width).toInt(),
- java.lang.Double.valueOf(doc.size().height).toInt(), CvType.CV_8UC4)
- Core.flip(doc.t(), endDoc, 1)
- Imgcodecs.imwrite(fileName, endDoc)
- endDoc.release()
+
+ val timeStamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(Date())
+ var displayName = "DOC-$timeStamp$imageSuffix"
+ if (scanTopic != null) {
+ displayName = "$scanTopic-$displayName"
+ }
+ val customFolderName = mSharedPref.getString("storage_folder", "OpenNoteScanner") ?: "OpenNoteScanner"
+
+ var savedFileUri: Uri? = null
+ var preQFilePath: String? = null // pre android Q file path
+
try {
- val exif = ExifInterface(fileName)
- exif.setAttribute("UserComment", "Generated using Open Note Scanner")
- val nowFormatted = mDateFormat.format(Date().time)
- exif.setAttribute(ExifInterface.TAG_DATETIME, nowFormatted)
- exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, nowFormatted)
- exif.setAttribute("Software", "OpenNoteScanner " + BuildConfig.VERSION_NAME + " https://goo.gl/2JwEPq")
- exif.saveAttributes()
- } catch (e: IOException) {
- e.printStackTrace()
- }
- if (isIntent && fileUri != null) {
- var inputStream: InputStream? = null
- var realOutputStream: OutputStream? = null
- try {
- inputStream = FileInputStream(fileName)
- realOutputStream = this.contentResolver.openOutputStream(fileUri)
- // Transfer bytes from in to out
- val buffer = ByteArray(1024)
- var len: Int
- while (inputStream.read(buffer).also { len = it } > 0) {
- realOutputStream!!.write(buffer, 0, len)
+ // if intent has no target uri, we just handle it as if it was a normal document scanned from the app
+ if (isIntentCapture && outputUriFromIntent != null) {
+ // this does not create any MediaStore entries, caller has to do that
+ // I also need to find the use case of calling the app via an intent, to better understand how to handle this case
+ savedFileUri = outputUriFromIntent
+ } else {
+ // Saving to gallery (MediaStore)
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
+ put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1) // Mark as pending until written
+ if (customFolderName.isNotBlank()) {
+ contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + customFolderName)
+ } else {
+ contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
+ }
+ val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+ savedFileUri = contentResolver.insert(imageCollection, contentValues)
+ } else {
+ val picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ val targetDir = if (customFolderName.isNotBlank()) {
+ File(picturesDir, customFolderName)
+ } else {
+ picturesDir
+ }
+
+ if (!targetDir.exists()) {
+ if (!targetDir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory: ${targetDir.absolutePath}")
+ // Fallback to default pictures directory if folder creation fails
+ preQFilePath = File(picturesDir, displayName).absolutePath
+ } else {
+ preQFilePath = File(targetDir, displayName).absolutePath
+ }
+ } else {
+ preQFilePath = File(targetDir, displayName).absolutePath
+ }
+ contentValues.put(MediaStore.Images.Media.DATA, preQFilePath)
+ // For pre-Q, insert into the legacy external content URI
+ savedFileUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+ }
+
+ if (savedFileUri == null) {
+ // insert failed
+ if (isIntentCapture) {
+ setResult(RESULT_CANCELED)
+ finish()
}
- } catch (e: FileNotFoundException) {
- e.printStackTrace()
- return
- } catch (e: IOException) {
- e.printStackTrace()
return
- } finally {
+ }
+
+ savedFileUri.let { uri ->
+ contentResolver.openOutputStream(uri)?.use { out ->
+ val endDoc = Mat()
+ Core.flip(doc.t(), endDoc, 1)
+
+ // Convert Mat to byte array
+ val matOfByte = org.opencv.core.MatOfByte()
+ val successEncode = Imgcodecs.imencode(imageSuffix, endDoc, matOfByte, encodingParams)
+ endDoc.release() // Release the temporary transformed Mat
+
+ if (!successEncode) {
+ throw IOException("Failed to encode Mat to $imageSuffix")
+ }
+ val imageBytes = matOfByte.toArray()
+ matOfByte.release()
+
+ out.write(imageBytes)
+ Log.d(TAG, "Successfully wrote image data.")
+ } ?: throw IOException("Failed to open created Media File.")
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isIntentCapture) {
+ val updateDetails = ContentValues().apply {
+ put(MediaStore.MediaColumns.IS_PENDING, 0) // Mark as complete
+ }
+ contentResolver.update(savedFileUri, updateDetails, null, null)
+ }
+
+ if (mimeType == "image/jpeg") {
try {
- inputStream!!.close()
- realOutputStream!!.close()
- } catch (e: IOException) {
- e.printStackTrace()
+ contentResolver.openFileDescriptor(savedFileUri, "rw")?.use { pfd ->
+ val exif = ExifInterface(pfd.fileDescriptor)
+ val nowFormatted = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) // Using a more EXIF-friendly date format
+ exif.setAttribute(ExifInterface.TAG_DATETIME, nowFormatted)
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, nowFormatted)
+ exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, nowFormatted)
+ exif.setAttribute(ExifInterface.TAG_SOFTWARE, "OpenNoteScanner " + BuildConfig.VERSION_NAME + " https://goo.gl/2JwEPq")
+ exif.setAttribute("UserComment", "Generated using Open Note Scanner")
+ exif.saveAttributes()
+ Log.d(TAG, "Exif data written to MediaStore URI.")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error writing Exif to MediaStore URI: ${e.message}", e)
}
}
- }
- Log.d(TAG, "wrote: $fileName")
- if (isIntent) {
- File(fileName).delete()
- setResult(RESULT_OK, intent)
- finish()
- } else {
- animateDocument(fileName, scannedDocument)
- Utils.addImageToGallery(fileName, this)
- }
- // Record goal "PictureTaken"
- TrackHelper.track().event("Picture", "PictureTaken").with(tracker)
- refreshCamera()
- }
+ if (isIntentCapture) {
+ if (outputUriFromIntent != null) {
+ setResult(RESULT_OK, intent)
+ } else {
+ setResult(RESULT_OK)
+ }
+ finish()
+ } else {
+ Log.d(TAG, "Document saved to MediaStore: $savedFileUri")
+ animateDocument(savedFileUri, scannedDocument)
- private fun createFileName(imgSuffix: String, folderName: String?): String {
- var fileName: String
- fileName = (Environment.getExternalStorageDirectory().toString()
- + "/" + folderName + "/")
- if (scanTopic != null) {
- fileName += "$scanTopic-"
+ TrackHelper.track().event("Picture", "PictureTaken").with(tracker)
+ refreshCamera()
+ }
+
+ } catch (e: Exception) {
+ Log.e(TAG, "Error saving document: ${e.message}", e)
+ if (savedFileUri != null) {
+ try {
+ contentResolver.delete(savedFileUri, null, null)
+ Log.w(TAG, "Attempted to delete MediaStore entry due to error: $savedFileUri")
+ // For Pre-Q, if preQFilePath is not null, also attempt to delete the physical file
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && preQFilePath != null) {
+ val physicalFile = File(preQFilePath)
+ if (physicalFile.exists()) {
+ if (physicalFile.delete()) {
+ Log.w(TAG, "Deleted physical file (pre-Q) due to error: $preQFilePath")
+ } else {
+ Log.e(TAG, "Failed to delete physical file (pre-Q) due to error: $preQFilePath")
+ }
+ }
+ }
+ } catch (deleteEx: Exception) {
+ Log.e(TAG, "Error during cleanup of MediaStore entry or file: ${deleteEx.message}")
+ }
+ }
+ if (isIntentCapture) {
+ setResult(RESULT_CANCELED)
+ finish()
+ } else {
+ // TODO: show error
+ refreshCamera()
+ }
+ } finally {
+ encodingParams.release()
}
- fileName += ("DOC-"
- + SimpleDateFormat("yyyyMMdd-HHmmss").format(Date())
- + imgSuffix)
- return fileName
}
- private fun animateDocument(filename: String, quadrilateral: ScannedDocument) {
- val runnable = AnimationRunnable(this, filename, quadrilateral)
+ private fun animateDocument(documentUri: Uri, quadrilateral: ScannedDocument) {
+ val runnable = AnimationRunnable(this, documentUri, quadrilateral)
runOnUiThread(runnable)
}
private fun shootSound() {
- val meng = getSystemService(AUDIO_SERVICE) as AudioManager
- val volume = meng.getStreamVolume(AudioManager.STREAM_NOTIFICATION)
+ val am = getSystemService(AUDIO_SERVICE) as AudioManager
+ val volume = am.getStreamVolume(AudioManager.STREAM_NOTIFICATION)
if (volume != 0) {
- if (_shootMP == null) {
- _shootMP = MediaPlayer.create(this, Uri.parse("file:///system/media/audio/ui/camera_click.ogg"))
- }
- if (_shootMP != null) {
- _shootMP!!.start()
+ if (mediaActionSound == null) {
+ mediaActionSound = MediaActionSound()
+ // Optional: Preload the sound for faster playback the first time.
+ // This is useful if you call shootSound frequently.
+ // mediaActionSound?.load(MediaActionSound.SHUTTER_CLICK)
}
+ mediaActionSound?.play(MediaActionSound.SHUTTER_CLICK)
}
}
diff --git a/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerApplication.kt b/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerApplication.kt
index d8ef2b8..e354f66 100644
--- a/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerApplication.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/OpenNoteScannerApplication.kt
@@ -49,7 +49,9 @@ class OpenNoteScannerApplication : MatomoApplication() {
mSharedPref.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener)
// When working on an app we don't want to skew tracking results.
- tracker.dryRunTarget = if (BuildConfig.DEBUG) Collections.synchronizedList(ArrayList()) else null
+ if (BuildConfig.DEBUG) {
+ tracker.dryRunTarget = Collections.synchronizedList(ArrayList())
+ }
// If you want to set a specific userID other than the random UUID token, do it NOW to ensure all future actions use that token.
// Changing it later will track new events as belonging to a different user.
diff --git a/app/src/main/java/com/todobom/opennotescanner/SettingsFragment.kt b/app/src/main/java/com/todobom/opennotescanner/SettingsFragment.kt
index 2e0292a..30a58a0 100644
--- a/app/src/main/java/com/todobom/opennotescanner/SettingsFragment.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/SettingsFragment.kt
@@ -34,7 +34,7 @@ class SettingsFragment : PreferenceFragment(), OnSharedPreferenceChangeListener
}
}
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
updatePreference(findPreference(key), key)
}
diff --git a/app/src/main/java/com/todobom/opennotescanner/helpers/AboutFragment.kt b/app/src/main/java/com/todobom/opennotescanner/helpers/AboutFragment.kt
index 8d36940..bf20394 100644
--- a/app/src/main/java/com/todobom/opennotescanner/helpers/AboutFragment.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/helpers/AboutFragment.kt
@@ -8,7 +8,8 @@ import androidx.fragment.app.DialogFragment
import com.todobom.opennotescanner.OpenNoteScannerApplication
import com.todobom.opennotescanner.R
import org.matomo.sdk.extra.TrackHelper
-import us.feras.mdv.MarkdownView
+import io.noties.markwon.Markwon
+import android.widget.TextView
class AboutFragment : DialogFragment() {
override fun onCreateView(
@@ -27,8 +28,17 @@ class AboutFragment : DialogFragment() {
val activity = activity ?: return
val window = dialog?.window ?: return
- val markdownView = view.findViewById(R.id.about_markdown)
- markdownView.loadMarkdownFile("file:///android_asset/" + getString(R.string.about_filename))
+ val markdownView = view.findViewById(R.id.about_markdown)
+ val markwon = Markwon.create(activity)
+
+ // Load markdown content from assets
+ try {
+ val inputStream = activity.assets.open(getString(R.string.about_filename))
+ val markdownContent = inputStream.bufferedReader().use { it.readText() }
+ markwon.setMarkdown(markdownView, markdownContent)
+ } catch (e: Exception) {
+ markdownView.text = "Error loading about content"
+ }
val size = Point()
activity.windowManager.defaultDisplay.getRealSize(size)
window.setLayout((size.x * 0.9).toInt(), (size.y * 0.9).toInt())
diff --git a/app/src/main/java/com/todobom/opennotescanner/helpers/AnimationRunnable.kt b/app/src/main/java/com/todobom/opennotescanner/helpers/AnimationRunnable.kt
index f51f4e0..0c1174b 100644
--- a/app/src/main/java/com/todobom/opennotescanner/helpers/AnimationRunnable.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/helpers/AnimationRunnable.kt
@@ -1,6 +1,7 @@
package com.todobom.opennotescanner.helpers
import android.graphics.Bitmap
+import android.net.Uri
import android.util.Log
import android.view.View
import android.view.animation.*
@@ -10,12 +11,12 @@ import com.todobom.opennotescanner.OpenNoteScannerActivity
import com.todobom.opennotescanner.R
import org.opencv.core.Point
import org.opencv.core.Size
+import kotlin.coroutines.coroutineContext
-class AnimationRunnable(private val activity: OpenNoteScannerActivity, filename: String, document: ScannedDocument) : Runnable {
+class AnimationRunnable(private val activity: OpenNoteScannerActivity, private val imageUri: Uri, document: ScannedDocument) : Runnable {
private val imageSize: Size = document.processed!!.size()
private val previewPoints: Array? = if(document.quadrilateral != null) document.previewPoints else null
private val previewSize: Size? = if(document.quadrilateral != null) document.previewSize else null
- private val fileName: String = filename
private var bitmap: Bitmap? = null
override fun run() {
@@ -62,7 +63,7 @@ class AnimationRunnable(private val activity: OpenNoteScannerActivity, filename:
params.height = height / 2
}
- bitmap = Utils.decodeSampledBitmapFromUri(fileName, params.width, params.height)
+ bitmap = Utils.decodeSampledBitmapFromUri(activity, imageUri, params.width, params.height)
imageView.setImageBitmap(bitmap)
imageView.visibility = View.VISIBLE
val translateAnimation = TranslateAnimation(
diff --git a/app/src/main/java/com/todobom/opennotescanner/helpers/CustomOpenCVLoader.kt b/app/src/main/java/com/todobom/opennotescanner/helpers/CustomOpenCVLoader.kt
deleted file mode 100644
index 800ea63..0000000
--- a/app/src/main/java/com/todobom/opennotescanner/helpers/CustomOpenCVLoader.kt
+++ /dev/null
@@ -1,202 +0,0 @@
-package com.todobom.opennotescanner.helpers
-
-import android.app.AlertDialog
-import android.app.Dialog
-import android.app.DownloadManager
-import android.content.*
-import android.net.Uri
-import android.os.Build
-import android.os.Environment
-import android.os.IBinder
-import android.provider.Settings
-import android.provider.Settings.SettingNotFoundException
-import android.util.Log
-import android.widget.Toast
-import androidx.core.content.FileProvider
-import com.todobom.opennotescanner.R
-import org.opencv.android.LoaderCallbackInterface
-import org.opencv.android.OpenCVLoader
-import java.io.File
-
-/**
- * Created by allgood on 22/02/16.
- */
-object CustomOpenCVLoader {
- private val dummyServiceConnection: ServiceConnection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, service: IBinder) {}
- override fun onServiceDisconnected(name: ComponentName) {}
- }
- private var myDownloadReference: Long = 0
- private var Callback: LoaderCallbackInterface? = null
- private var Version: String? = null
- private var mAskInstallDialog: AlertDialog? = null
- fun isGooglePlayInstalled(context: Context): Boolean {
- val pm = context.packageManager
-
- // DISABLED installation from Google Play since OpenCV Manager is removed from there
- /*
- try
- {
- PackageInfo info = pm.getPackageInfo("com.android.vending", PackageManager.GET_ACTIVITIES);
- String label = (String) info.applicationInfo.loadLabel(pm);
- app_installed = (label != null && label.equals("Google Play Store"));
- }
- catch (PackageManager.NameNotFoundException e)
- {
- app_installed = false;
- }
- */return false
- }
-
- fun isOpenCVInstalled(Version: String?, AppContext: Context): Boolean {
- val intent = Intent("org.opencv.engine.BIND")
- intent.setPackage("org.opencv.engine")
- val result = AppContext.bindService(intent, dummyServiceConnection, Context.BIND_AUTO_CREATE)
- AppContext.unbindService(dummyServiceConnection)
- return result
- }
-
- var onComplete: MyBroadcastReceiver? = null
- var waitOpenCVDialog: Dialog? = null
- fun initAsync(version: String?, AppContext: Context, callback: LoaderCallbackInterface?): Boolean {
- Version = version
- Callback = callback
-
- // if dialog is showing, remove
- mAskInstallDialog?.dismiss()
- mAskInstallDialog = null
-
- // if don't have google play, check for OpenCV before trying to init
- if (!isOpenCVInstalled(Version, AppContext)) {
- var isNonPlayAppAllowed = false
- try {
- isNonPlayAppAllowed = Settings.Secure.getInt(AppContext.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS) == 1
- } catch (e: SettingNotFoundException) {
- e.printStackTrace()
- }
- val askInstallOpenCV = AlertDialog
- .Builder(AppContext)
- .setTitle(R.string.install_opencv)
- .setMessage(R.string.ask_install_opencv)
- .setCancelable(false)
-
- if (isNonPlayAppAllowed) {
- askInstallOpenCV.setNeutralButton(R.string.githubdownload, object : DialogInterface.OnClickListener {
- var arch = Build.SUPPORTED_ABIS[0]
- override fun onClick(dialog: DialogInterface, which: Int) {
- val sAndroidUrl = "https://github.com/ctodobom/opencv/releases/download/3.1.0/OpenCV_3.1.0_Manager_3.10_$arch.apk"
- onComplete = MyBroadcastReceiver(AppContext)
- AppContext.registerReceiver(onComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
- val dm = AppContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
- val request = DownloadManager.Request(Uri.parse(sAndroidUrl))
- val sDest = "file://" + Environment.getExternalStorageDirectory().toString() + "/Download/OpenCV_3.1.0_Manager_3.10_" + arch + ".apk"
- request.setDestinationUri(Uri.parse(sDest))
- myDownloadReference = dm.enqueue(request)
- dialog.dismiss()
- waitOpenCVDialog = AlertDialog
- .Builder(AppContext)
- .setTitle(R.string.downloading)
- .setMessage(R.string.downloading_opencv)
- .setCancelable(false)
- .setOnCancelListener { dialog1: DialogInterface ->
- dm.remove(myDownloadReference)
- AppContext.unregisterReceiver(onComplete)
- dialog1.dismiss()
- mAskInstallDialog = null
- }
- .setNegativeButton(R.string.answer_cancel) { dialog12: DialogInterface, which1: Int ->
- dm.remove(myDownloadReference)
- AppContext.unregisterReceiver(onComplete)
- dialog12.dismiss()
- mAskInstallDialog = null
- }
- .create()
- .also {
- it.show()
- }
- }
- })
- }
- if (isGooglePlayInstalled(AppContext)) {
- askInstallOpenCV.setPositiveButton(R.string.googleplay) { dialog: DialogInterface, which: Int ->
- dialog.dismiss()
- AppContext.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=org.opencv.engine")))
- }
- }
- if (!isNonPlayAppAllowed && !isGooglePlayInstalled(AppContext)) {
- askInstallOpenCV.setMessage("""
- ${AppContext.getString(R.string.ask_install_opencv)}
-
- ${AppContext.getString(R.string.messageactivateunknown)}
- """.trimIndent()
- )
- askInstallOpenCV.setNeutralButton(R.string.activateunknown) { dialog: DialogInterface, which: Int ->
- dialog.dismiss()
- AppContext.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
- }
- }
- mAskInstallDialog = askInstallOpenCV.create().also {
- it.show()
- }
- } else {
- // initialize opencv
- return OpenCVLoader.initAsync(Version, AppContext, Callback)
- }
- return false
- }
-
- class MyBroadcastReceiver(private val AppContext: Context) : BroadcastReceiver() {
- override fun onReceive(ctxt: Context, intent: Intent) {
- val id = intent.extras!!.getLong(DownloadManager.EXTRA_DOWNLOAD_ID)
- if (id == myDownloadReference) {
- val dm = AppContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
- val query = DownloadManager.Query()
- query.setFilterById(id)
- val cursor = dm.query(query)
- if (cursor.moveToFirst()) {
- // get the status of the download
- val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
- when (val status = cursor.getInt(columnIndex)) {
- DownloadManager.STATUS_SUCCESSFUL -> {
- val downloadFileLocalUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
- val apkFile: File
- apkFile = File(Uri.parse(downloadFileLocalUri).path)
- waitOpenCVDialog?.dismiss()
- AppContext.unregisterReceiver(onComplete)
- val uri: Uri
- val installIntent: Intent
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
- uri = FileProvider.getUriForFile(ctxt, ctxt.applicationContext.packageName + ".fileprovider", apkFile)
- } else {
- installIntent = Intent(Intent.ACTION_VIEW)
- uri = Uri.fromFile(apkFile)
- }
- installIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
- installIntent.setDataAndType(uri, "application/vnd.android.package-archive")
- AppContext.startActivity(installIntent)
- }
- DownloadManager.STATUS_FAILED -> {
- // get the reason - more detail on the status
- val columnReason = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
- val reason = cursor.getInt(columnReason)
- Toast.makeText(AppContext,
- "FAILED: $reason",
- Toast.LENGTH_LONG).show()
- AppContext.unregisterReceiver(onComplete)
- }
- else -> Log.d("CustomOpenCVLoader", "Received download manager status: $status")
- }
- } else {
- Log.d("CustomOpenCVLoader", "missing download")
- AppContext.unregisterReceiver(onComplete)
- }
- cursor.close()
- }
- }
-
- companion object {
- private const val TAG = "CustomOpenCVLoader"
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/todobom/opennotescanner/helpers/PdfHelper.kt b/app/src/main/java/com/todobom/opennotescanner/helpers/PdfHelper.kt
index 098ab12..39255fd 100644
--- a/app/src/main/java/com/todobom/opennotescanner/helpers/PdfHelper.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/helpers/PdfHelper.kt
@@ -1,10 +1,15 @@
package com.todobom.opennotescanner.helpers
+import android.content.ContentValues
import android.content.Context
+import android.media.MediaScannerConnection
+import android.net.Uri
+import android.os.Build
import android.os.Environment
import android.preference.PreferenceManager
+import android.provider.MediaStore
+import android.util.Log
import android.widget.Toast
-import com.itextpdf.io.image.ImageData
import com.itextpdf.io.image.ImageDataFactory
import com.itextpdf.kernel.geom.PageSize
import com.itextpdf.kernel.pdf.PdfDocument
@@ -13,55 +18,130 @@ import com.itextpdf.layout.Document
import com.itextpdf.layout.element.Image
import com.todobom.opennotescanner.R
import java.io.File
-import java.io.FileNotFoundException
-import java.net.MalformedURLException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
object PdfHelper {
+ private const val TAG = "OpenNoteScanner-Pdf"
+
@JvmStatic
- fun mergeImagesToPdf(applicationContext: Context, files: ArrayList): String? {
- //TODO move this to background thread
- if (files.isEmpty()) {
- Toast
- .makeText(applicationContext, applicationContext.getString(R.string.no_files_selected), Toast.LENGTH_SHORT)
- .show()
+ fun mergeImagesToPdf(applicationContext: Context, imageUris: ArrayList): Uri? {
+ if (imageUris.isEmpty()) {
+ Toast.makeText(
+ applicationContext,
+ applicationContext.getString(R.string.no_files_selected),
+ Toast.LENGTH_SHORT
+ ).show()
return null
}
- val outputFile = ("PDF-"
- + SimpleDateFormat("yyyyMMdd-HHmmss").format(Date()) + ".pdf")
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
- val pdfFilePath = File(
- Environment.getExternalStorageDirectory()
- .toString() + File.separator + sharedPreferences.getString("storage_folder", "OpenNoteScanner"), outputFile)
- .absolutePath
- var pdfWriter: PdfWriter? = null
+
+ val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ val customFolderName = preferences.getString("storage_folder", "OpenNoteScanner") ?: "OpenNoteScanner"
+
+ val resolver = applicationContext.contentResolver
+
+ val displayName = "PDF-${SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(Date())}.pdf"
+ var outputStream: OutputStream? = null
+ var pdfUri: Uri? = null
+ var preQFile: File? = null
+
try {
- pdfWriter = PdfWriter(pdfFilePath)
- } catch (e: FileNotFoundException) {
- e.printStackTrace()
- }
- if (pdfWriter == null) {
- return null
- }
- val pdfDocument = PdfDocument(pdfWriter)
- val document = Document(pdfDocument)
- files.sort()
- for (file in files) {
- var imageData: ImageData? = null
- try {
- imageData = ImageDataFactory.create(file)
- } catch (e: MalformedURLException) {
- e.printStackTrace()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
+ put(MediaStore.MediaColumns.IS_PENDING, 1) // Mark as pending until written
+ if (customFolderName.isNotBlank()) {
+ put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + File.separator + "OpenNoteScanner")
+ } else {
+ put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS)
+ }
+ }
+
+ pdfUri = resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), contentValues)
+ pdfUri?.let {
+ outputStream = resolver.openOutputStream(it)
+ }
+ } else {
+ // Fallback for pre-Android Q
+ val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
+ val targetDir = if (customFolderName.isNotBlank()) File(storageDir, customFolderName) else storageDir
+
+ if (!targetDir.exists()) {
+ if (!targetDir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory: ${targetDir.absolutePath}")
+ Toast.makeText(applicationContext, "Could not create directory", Toast.LENGTH_LONG).show()
+ return null
+ }
+ }
+
+ preQFile = File(targetDir, displayName)
+ pdfUri = Uri.fromFile(preQFile)
+ outputStream = FileOutputStream(preQFile)
}
- if (imageData == null) {
+
+ if (outputStream == null) {
+ Toast.makeText(applicationContext, "Failed to create PDF output stream", Toast.LENGTH_LONG).show()
return null
}
- val image = Image(imageData)
- pdfDocument.addNewPage(PageSize(image.imageWidth, image.imageHeight))
- document.add(image)
+
+ val pdfWriter = PdfWriter(outputStream)
+ val pdfDocument = PdfDocument(pdfWriter)
+ val document = Document(pdfDocument)
+
+ // sorts by date
+ imageUris.sort()
+
+ for (imageUri in imageUris) {
+ try {
+ applicationContext.contentResolver.openInputStream(imageUri)?.use { inputStream ->
+ val imageData = ImageDataFactory.create(inputStream.readBytes())
+ val image = Image(imageData)
+
+ pdfDocument.addNewPage(PageSize(image.imageWidth, image.imageHeight))
+ document.add(image)
+ } ?: throw IOException("Failed to open stream for URI: $imageUri")
+ } catch (e: Exception) { // Catch more general exceptions during image processing
+ e.printStackTrace()
+ // TODO: show error
+ // Decide if you want to skip this image or abort the PDF creation
+ }
+ }
+ document.close()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (pdfUri != null) {
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.IS_PENDING, 0)
+ }
+ resolver.update(pdfUri, contentValues, null, null)
+ }
+ } else {
+ // for good measures
+ MediaScannerConnection.scanFile(applicationContext, arrayOf(preQFile?.absolutePath), arrayOf("application/pdf"), null)
+ }
+
+ Toast.makeText(applicationContext, "PDF saved to ${pdfUri?.path ?: displayName}", Toast.LENGTH_LONG).show()
+ return pdfUri
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Toast.makeText(applicationContext, "Error writing PDF: ${e.message}", Toast.LENGTH_LONG).show()
+ // Clean up if a MediaStore entry was created but writing failed
+ pdfUri?.let { uri ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ try {
+ applicationContext.contentResolver.delete(uri, null, null)
+ } catch (deleteEx: Exception) {
+ deleteEx.printStackTrace()
+ }
+ }
+ }
}
- document.close()
- return pdfFilePath
+
+ return null
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/todobom/opennotescanner/helpers/Utils.kt b/app/src/main/java/com/todobom/opennotescanner/helpers/Utils.kt
index 430b486..584605c 100644
--- a/app/src/main/java/com/todobom/opennotescanner/helpers/Utils.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/helpers/Utils.kt
@@ -1,18 +1,22 @@
package com.todobom.opennotescanner.helpers
-import android.content.ContentValues
+import android.content.ContentUris
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Point
+import android.net.Uri
+import android.os.Build
import android.os.Environment
import android.preference.PreferenceManager
import android.provider.MediaStore
+import android.util.Log
import android.view.WindowManager
import com.todobom.opennotescanner.OpenNoteScannerActivity
import java.io.File
+import java.io.InputStream
import java.util.*
import java.util.regex.Pattern
import javax.microedition.khronos.egl.EGL10
@@ -24,74 +28,64 @@ class Utils(
) {
private val mSharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(_context)
- /*
- * Reading file paths from SDCard
- */
- val filePaths: ArrayList
+ val fileUris: ArrayList
get() {
- val filePaths = ArrayList()
- val directory = File(
- Environment.getExternalStorageDirectory()
- .toString() + File.separator + mSharedPref.getString("storage_folder", "OpenNoteScanner"))
-
- // check for directory
- if (directory.isDirectory) {
- // getting list of file paths
- val listFiles = directory.listFiles()
- Arrays.sort(listFiles) { f1, f2 -> f2.name.compareTo(f1.name) }
-
- // Check for count
- if (listFiles.size > 0) {
-
- // loop through all files
- for (i in listFiles.indices) {
-
- // get file path
- val filePath = listFiles[i].absolutePath
-
- // check for supported file extension
- if (isSupportedFile(filePath)) {
- // Add image path to array list
- filePaths.add(filePath)
- }
- }
- }
+ val imageUris = ArrayList()
+ val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
+ } else {
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
- return filePaths
- }
- /*
- * Check supported file extensions
- *
- * @returns boolean
- */
- private fun isSupportedFile(filePath: String): Boolean {
- val ext = filePath.substring(filePath.lastIndexOf(".") + 1,
- filePath.length)
- return AppConstant.FILE_EXTN.contains(ext.toLowerCase(Locale.getDefault()))
- }// Older device
-
- /*
- * getting screen width
- */
- val screenWidth: Int
- get() {
- val columnWidth: Int
- val wm = _context
- .getSystemService(Context.WINDOW_SERVICE) as WindowManager
- val display = wm.defaultDisplay
- val point = Point()
- try {
- display.getSize(point)
- } catch (ignore: NoSuchMethodError) { // Older device
- point.x = display.width
- point.y = display.height
+ val projection = arrayOf(
+ MediaStore.Images.Media._ID,
+ MediaStore.Images.Media.DISPLAY_NAME,
+ MediaStore.Images.Media.DATA // For pre-Q compatibility and direct paths, FIXME: switch to only URI
+ )
+
+ val appFolderName = mSharedPref.getString("storage_folder", "OpenNoteScanner")
+ val selection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?"
+ } else {
+ "${MediaStore.Images.Media.DATA} LIKE ?"
+ }
+ val selectionArgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // post-Q, partial path matching
+ arrayOf("%${Environment.DIRECTORY_PICTURES}/$appFolderName/%")
+ } else {
+ // pre-Q, direct path matching
+ val legacyPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).absolutePath +
+ File.separator + appFolderName
+ arrayOf("$legacyPath/%")
+ }
+
+ // Sort order
+ val sortOrder = "${MediaStore.Images.Media.DATE_MODIFIED} DESC" // Or DATE_ADDED, DISPLAY_NAME ASC
+
+ _context.contentResolver.query(
+ collection,
+ projection,
+ selection,
+ selectionArgs,
+ sortOrder
+ )?.use { cursor ->
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(idColumn)
+ val contentUri: Uri = ContentUris.withAppendedId(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ id
+ )
+ imageUris.add(contentUri)
+ }
}
- columnWidth = point.x
- return columnWidth
+ return imageUris
}
companion object {
+
+ private const val TAG = "OpenNoteScanner-Utils"
+
@JvmStatic
val maxTextureSize: Int
get() {
@@ -133,7 +127,7 @@ class Utils(
}
@JvmStatic
- fun isMatch(s: String?, pattern: String?): Boolean {
+ fun isMatch(s: String, pattern: String): Boolean {
return try {
val patt = Pattern.compile(pattern)
val matcher = patt.matcher(s)
@@ -143,20 +137,34 @@ class Utils(
}
}
- fun decodeSampledBitmapFromUri(path: String?, reqWidth: Int, reqHeight: Int): Bitmap? {
- var bm: Bitmap? = null
- // First decode with inJustDecodeBounds=true to check dimensions
- val options = BitmapFactory.Options()
- options.inJustDecodeBounds = true
- BitmapFactory.decodeFile(path, options)
+ fun decodeSampledBitmapFromUri(context: Context, uri: Uri, reqWidth: Int, reqHeight: Int): Bitmap? {
+ var inputStream: InputStream? = null
+ try {
+ // First decode with inJustDecodeBounds=true to check dimensions
+ val options = BitmapFactory.Options()
+ options.inJustDecodeBounds = true
+ inputStream = context.contentResolver.openInputStream(uri)
+ if (inputStream == null) {
+ Log.e(TAG, "decodeSampledBitmapFromUri: Could not open InputStream for URI: $uri")
+ return null
+ }
+ BitmapFactory.decodeStream(inputStream, null, options)
+ inputStream.close()
- // Calculate inSampleSize
- options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
+ // calculate downsample factor
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
- // Decode bitmap with inSampleSize set
- options.inJustDecodeBounds = false
- bm = BitmapFactory.decodeFile(path, options)
- return bm
+ options.inJustDecodeBounds = false
+ inputStream = context.contentResolver.openInputStream(uri)
+ if (inputStream == null) {
+ Log.e(TAG, "decodeSampledBitmapFromUri: Could not reopen InputStream for URI: $uri")
+ return null
+ }
+ val bitmap = BitmapFactory.decodeStream(inputStream, null, options)
+ return bitmap
+ } finally {
+ inputStream?.close()
+ }
}
fun calculateInSampleSize(
@@ -176,35 +184,29 @@ class Utils(
return inSampleSize
}
- fun addImageToGallery(filePath: String?, context: Context) {
- val values = ContentValues()
- values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
- values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
- values.put(MediaStore.MediaColumns.DATA, filePath)
- context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
- }
-
@JvmStatic
- fun removeImageFromGallery(filePath: String, context: Context) {
- context.contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- MediaStore.Images.Media.DATA
- + "='"
- + filePath
- + "'", null)
+ fun removeImageFromGallery(fileUri: Uri, context: Context) {
+ try {
+ val rowsDeleted = context.contentResolver.delete(fileUri, null, null)
+ if (rowsDeleted == 0) {
+ Log.w(TAG, "Nothing deleted for: $fileUri")
+ }
+ } catch (e: SecurityException) {
+ // we should have permissions
+ e.printStackTrace()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
}
@JvmStatic
fun isPackageInstalled(context: Context, packagename: String): Boolean {
- val pm = context.packageManager
- var app_installed = false
- app_installed = try {
- val info = pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES)
- val label = info.applicationInfo.loadLabel(pm) as String
- label != null
- } catch (e: PackageManager.NameNotFoundException) {
+ return try {
+ context.packageManager.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES)
+ true
+ } catch (_: PackageManager.NameNotFoundException) {
false
}
- return app_installed
}
diff --git a/app/src/main/java/com/todobom/opennotescanner/views/TagEditorFragment.kt b/app/src/main/java/com/todobom/opennotescanner/views/TagEditorFragment.kt
index b4e4878..4fd2b9f 100644
--- a/app/src/main/java/com/todobom/opennotescanner/views/TagEditorFragment.kt
+++ b/app/src/main/java/com/todobom/opennotescanner/views/TagEditorFragment.kt
@@ -1,6 +1,7 @@
package com.todobom.opennotescanner.views
import android.content.res.ColorStateList
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -8,20 +9,27 @@ import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.ImageView
+import androidx.core.net.toFile
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.DialogFragment
import com.todobom.opennotescanner.R
+import java.io.File
import java.io.IOException
/**
* Created by allgood on 29/05/16.
*/
-class TagEditorFragment : DialogFragment() {
+class TagEditorFragment(val fileUri: Uri) : DialogFragment() {
private var mRunOnDetach: Runnable? = null
- private var filePath: String? = null
var stdTagsState = BooleanArray(7)
var stdTags = arrayOf("rocket", "gift", "tv", "bell", "game", "star", "magnet")
var stdTagsButtons = arrayOfNulls(7)
+
+ init {
+ retainInstance = true
+ loadTags()
+ }
+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val tagEditorView = inflater.inflate(R.layout.tageditor_view, container)
@@ -59,58 +67,60 @@ class TagEditorFragment : DialogFragment() {
}
private fun loadTags() {
- var exif: ExifInterface? = null
- try {
- exif = ExifInterface(filePath!!)
+ val exif = try {
+ context?.contentResolver?.openInputStream(fileUri)?.use {
+ ExifInterface(it)
+ }
} catch (e: IOException) {
e.printStackTrace()
+ return
}
- val userComment = exif!!.getAttribute("UserComment")
+ if (exif == null) return
+
+ val userComment = exif.getAttribute(ExifInterface.TAG_USER_COMMENT)
for (i in 0..6) {
stdTagsState[i] = userComment!!.contains("<" + stdTags[i] + ">")
}
}
private fun saveTags() {
- var exif: ExifInterface? = null
- try {
- exif = ExifInterface(filePath!!)
- } catch (e: IOException) {
- e.printStackTrace()
- }
- var userComment = exif!!.getAttribute("UserComment")
- for (i in 0..6) {
- if (stdTagsState[i] && !userComment!!.contains("<" + stdTags[i] + ">")) {
- userComment += "<" + stdTags[i] + ">"
- } else if (!stdTagsState[i] && userComment!!.contains("<" + stdTags[i] + ">")) {
- userComment!!.replace("<" + stdTags[i] + ">".toRegex(), "")
+ context?.let { context ->
+ // save as temp file
+ val inputStream = context.contentResolver?.openInputStream(fileUri)
+ if (inputStream == null) return
+ val tempFile = File.createTempFile("exif-edit", null, context.cacheDir)
+ try {
+ inputStream.use { input -> tempFile.outputStream().use { input.copyTo(it) } }
+
+ // Open temp file for exif edit
+ val exif = ExifInterface(tempFile.absolutePath)
+ var userComment = exif.getAttribute("UserComment")
+ for (i in 0..6) {
+ if (stdTagsState[i] && !userComment!!.contains("<" + stdTags[i] + ">")) {
+ userComment += "<" + stdTags[i] + ">"
+ } else if (!stdTagsState[i] && userComment!!.contains("<" + stdTags[i] + ">")) {
+ userComment!!.replace("<" + stdTags[i] + ">".toRegex(), "")
+ }
+ }
+ exif.setAttribute(ExifInterface.TAG_USER_COMMENT, userComment)
+ exif.saveAttributes()
+
+ // replace original with exif edited file
+ context.contentResolver.openOutputStream(fileUri, "w")?.use { out ->
+ tempFile.inputStream().use { input -> input.copyTo(out) }
+ }
+ } finally {
+ tempFile.delete()
}
}
- exif.setAttribute("UserComment", userComment)
- try {
- exif.saveAttributes()
- } catch (e: IOException) {
- e.printStackTrace()
- }
}
override fun onDetach() {
super.onDetach()
- if (mRunOnDetach != null) {
- mRunOnDetach!!.run()
- }
+ mRunOnDetach?.run()
}
fun setRunOnDetach(runOnDetach: Runnable?) {
mRunOnDetach = runOnDetach
}
-
- fun setFilePath(filePath: String?) {
- this.filePath = filePath
- loadTags()
- }
-
- init {
- retainInstance = true
- }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/about_view.xml b/app/src/main/res/layout/about_view.xml
index d1d6f07..d47af84 100644
--- a/app/src/main/res/layout/about_view.xml
+++ b/app/src/main/res/layout/about_view.xml
@@ -10,13 +10,15 @@
android:layout_height="fill_parent"
android:gravity="top">
-
Fertig
Bitte geben Sie ein Thema an
+ Open Note Scanner benötgit die folgenden Berechtigungen
+ Open Note Scanner muss für den Betrieb auf die Kamera und Gerätespeicher zugreifen
+ Ok
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9aa18b6..16b3004 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -89,4 +89,7 @@
US Letter
Disabled
Custom
+ Open Note Scanner requires these permissions to function
+ Open Note Scanner requires access to the Camera and Storage to run
+ Ok
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index 8d13fa1..2deee69 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/build.gradle.kts b/build.gradle.kts
index 4cee2a4..82b4f56 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,14 +3,13 @@
buildscript {
repositories {
mavenCentral()
- jcenter()
google()
maven(url = "https://jitpack.io")
}
dependencies {
- classpath("com.android.tools.build:gradle:7.4.2")
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
+ classpath("com.android.tools.build:gradle:8.11.1")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -18,5 +17,5 @@ buildscript {
}
tasks.register("clean", Delete::class) {
- delete(rootProject.buildDir)
+ delete(rootProject.layout.buildDirectory)
}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index ccebba7..d64cd49 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a2efda8..37f853b 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=47a5bfed9ef814f90f8debcbbb315e8e7c654109acd224595ea39fca95c5d4da
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 79a61d4..1aa94a4 100755
--- a/gradlew
+++ b/gradlew
@@ -83,10 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,10 +131,13 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done
fi
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 15a0f92..95de5c4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -2,7 +2,6 @@ dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
- jcenter()
google()
maven(url = "https://jitpack.io")
}