From 53158f64dd6bcddd0acfe204ff629dfee14d3e4b Mon Sep 17 00:00:00 2001 From: Sauvio Date: Sat, 2 Dec 2023 01:14:32 +0800 Subject: [PATCH 1/7] feat: Compatible with TaskerPlugin fix: Crash caused by selecting multiple languages docs: Update README.md chore: Upgrade dependencies on related libraries --- README.md | 2 + app/build.gradle | 20 +- app/proguard-rules.pro | 28 + app/src/main/AndroidManifest.xml | 11 +- .../github/subhamtyagi/ocr/MainActivity.java | 42 +- .../ocr/tasker/ActivityConfigTasker.kt | 57 + .../github/subhamtyagi/ocr/tasker/OCRAPI.kt | 25 + .../subhamtyagi/ocr/tasker/OCRConfig.kt | 107 ++ .../github/subhamtyagi/ocr/tasker/OCRInput.kt | 30 + .../subhamtyagi/ocr/tasker/OCROutput.kt | 13 + .../subhamtyagi/ocr/tasker/OCRRunner.kt | 37 + .../io/github/subhamtyagi/ocr/tasker/Util.kt | 43 + .../subhamtyagi/ocr/utils/Constants.java | 1 + .../github/subhamtyagi/ocr/utils/Utils.java | 10 +- .../drawable/ic_baseline_engine_mode_32.xml | 5 + .../res/drawable/ic_launcher_background.xml | 80 ++ app/src/main/res/drawable/plugin.png | Bin 0 -> 863 bytes .../main/res/layout/activity_config_ocr.xml | 43 + app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-be/strings.xml | 1 + app/src/main/res/values-bn/strings.xml | 1 + app/src/main/res/values-cn/strings.xml | 6 + app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-he/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-id/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-nb-rNO/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-si/strings.xml | 1 + app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-zgh/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/arrays.xml | 12 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/xml/root_preferences.xml | 9 + build.gradle | 7 +- cropper/build.gradle | 15 +- .../edmodo/cropper/BitmapUtils.java | 25 +- .../edmodo/cropper/CropFileProvider.java | 6 +- gradle.properties | 28 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 +- taskerpluginlibrary/.gitignore | 1 + taskerpluginlibrary/build.gradle | 43 + taskerpluginlibrary/proguard-rules.pro | 35 + .../src/main/AndroidManifest.xml | 55 + .../taskerpluginlibrary/Exceptions.kt | 6 + .../taskerpluginlibrary/SimpleResult.kt | 31 + .../TaskerPluginConstants.kt | 23 + .../com/joaomgcd/taskerpluginlibrary/Util.kt | 54 + .../action/ActionReceivers.kt | 32 + .../action/TaskerPluginRunnerAction.kt | 52 + .../TaskerPluginRunnerActionVariants.kt | 8 + .../condition/ConditionReceivers.kt | 45 + .../condition/TaskerPluginConditionResult.kt | 6 + .../condition/TaskerPluginRunnerCondition.kt | 145 +++ .../TaskerPluginRunnerConditionVariants.kt | 25 + .../config/HostCapabilities.kt | 28 + .../config/TaskerPluginConfig.kt | 21 + .../config/TaskerPluginConfigHelper.kt | 97 ++ .../TaskerPluginConfigHelperVariants.kt | 46 + .../extensions/Internal.kt | 107 ++ .../taskerpluginlibrary/extensions/Public.kt | 50 + .../input/TaskerInputField.kt | 16 + .../input/TaskerPluginInput.kt | 160 +++ .../output/TaskerOutputVariable.kt | 13 + .../output/TaskerPluginOutputBase.kt | 99 ++ .../output/TaskerPluginOutputForConfig.kt | 27 + .../runner/TaskerPluginOutputForRunner.kt | 105 ++ .../runner/TaskerPluginOutputValueGetter.kt | 40 + .../runner/IntentServiceParallel.kt | 96 ++ .../runner/TaskerOutputRenames.kt | 13 + .../runner/TaskerPluginResult.kt | 37 + .../runner/TaskerPluginResultCondition.kt | 25 + .../runner/TaskerPluginRunner.kt | 125 ++ .../android/tasker/PluginResultReceiver.kt | 7 + .../android/tasker/TaskerIntent.java | 449 ++++++++ .../android/tasker/TaskerPlugin.java | 1005 +++++++++++++++++ .../src/main/res/mipmap/ic_launcher.png | Bin 0 -> 2096 bytes .../src/main/res/values/strings.xml | 12 + 91 files changed, 3790 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/ActivityConfigTasker.kt create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRInput.kt create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/tasker/Util.kt create mode 100644 app/src/main/res/drawable/ic_baseline_engine_mode_32.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/plugin.png create mode 100644 app/src/main/res/layout/activity_config_ocr.xml create mode 100644 taskerpluginlibrary/.gitignore create mode 100644 taskerpluginlibrary/build.gradle create mode 100644 taskerpluginlibrary/proguard-rules.pro create mode 100644 taskerpluginlibrary/src/main/AndroidManifest.xml create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Exceptions.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/SimpleResult.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/TaskerPluginConstants.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Util.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/ActionReceivers.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerAction.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerActionVariants.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/ConditionReceivers.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginConditionResult.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerCondition.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerConditionVariants.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/HostCapabilities.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfig.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelper.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelperVariants.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Internal.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Public.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerInputField.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerPluginInput.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerOutputVariable.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputBase.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputForConfig.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputForRunner.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputValueGetter.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/IntentServiceParallel.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerOutputRenames.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResult.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResultCondition.kt create mode 100644 taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginRunner.kt create mode 100644 taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/PluginResultReceiver.kt create mode 100644 taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerIntent.java create mode 100644 taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java create mode 100644 taskerpluginlibrary/src/main/res/mipmap/ic_launcher.png create mode 100644 taskerpluginlibrary/src/main/res/values/strings.xml diff --git a/README.md b/README.md index e745c7a9..eebb8a2a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This App is based on Features +* Compatible with Tasker plugin [TaskerPlugin](https://github.com/joaomgcd/TaskerPluginSample/blob/master/README.md). * Extract Text From Images. * Copy data to Clipboard. * Select any part of Text. @@ -37,6 +38,7 @@ This App is based on Code Contributors +* Sauvio * Shubham Tyagi * New UI is designed by [Hannes Gehrold](https://github.com/h4n23s) * [urlordjames](https://github.com/urlordjames) diff --git a/app/build.gradle b/app/build.gradle index 9289feb4..c7115fdd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,14 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { - compileSdkVersion 33 + compileSdkVersion 34 def gitUrl = getRepositoryURL() defaultConfig { applicationId "io.github.subhamtyagi.ocr" minSdkVersion 18 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 12 versionName "4.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -65,21 +66,28 @@ android { compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility "$java_version" + targetCompatibility "$java_version" } + namespace 'io.github.subhamtyagi.ocr' + buildFeatures { + viewBinding true + } + } dependencies { - + implementation project(':taskerpluginlibrary') implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation project(':cropper') implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' - // implementation 'cz.adaptech:tesseract4android:4.1.1' + // implementation 'cz.adaptech:tesseract4android:4.1.1' implementation 'cz.adaptech.tesseract4android:tesseract4android-openmp:4.3.0' implementation 'com.google.android.material:material:1.9.0-alpha01' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index aebab107..21c99898 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -37,3 +37,31 @@ #-obfuscationdictionary #-classobfuscationdictionary #-packageobfuscationdictionary + +-keepattributes SourceFile,LineNumberTable +-keepattributes *Annotation* +-keep public class com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject { *; } +-keep public class com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable { *; } + +-keepclasseswithmembers class * { + @com.joaomgcd.taskerpluginlibrary.input.TaskerInputField ; +} +-keep @com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot public class * +-keep @com.joaomgcd.taskerpluginlibrary.input.TaskerInputObject public class * +-keep @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject public class * +-keepclassmembers class * { + @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject *; +} +-keepclassmembers class * { + @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable *; +} +-keepclassmembers @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject class * { *; } +-keep public class * extends com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner { *; } + + +-keep public class net.dinglisch.android.tasker.PluginResultReceiver { *; } + +-dontwarn android.** +-dontwarn com.google.** + +-keep public class kotlin.Unit { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d7ca9724..0e144635 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,9 @@ + - @@ -65,6 +65,15 @@ android:name="com.theartofdev.edmodo.cropper.CropImageActivity" android:exported="true" android:theme="@style/Base.Theme.AppCompat" /> + + + + + \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java index 660e0c37..f2e68574 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java @@ -12,6 +12,7 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.text.Html; @@ -33,6 +34,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.googlecode.tesseract.android.TessBaseAPI; +import com.theartofdev.edmodo.cropper.CropFileProvider; import com.theartofdev.edmodo.cropper.CropImage; import com.theartofdev.edmodo.cropper.CropImageView; @@ -212,7 +214,7 @@ private void initIntent() { showLanguageSelectionDialog(imageUri); } } - } else if (action.equals("screenshot")) { + } else if ("screenshot".equals(action)) { // uri } } @@ -233,23 +235,23 @@ private void showLanguageSelectionDialog(Uri imageUri) { RadioButton radioButton3 = view.findViewById(R.id.rb_language3); String[] la = Utils.getLast3UsedLanguage(); - radioButton1.setText(getLanguageNameFromCode(la[0])); - radioButton2.setText(getLanguageNameFromCode(la[1])); - radioButton3.setText(getLanguageNameFromCode(la[2])); + radioButton1.setText(getLanguageNameFromCode(la[0], false)); + radioButton2.setText(getLanguageNameFromCode(la[1], false)); + radioButton3.setText(getLanguageNameFromCode(la[2], false)); radioButton1.setOnClickListener(view1 -> startOCRFromShareMenu(la[0], imageUri)); radioButton2.setOnClickListener(view1 -> startOCRFromShareMenu(la[1], imageUri)); radioButton3.setOnClickListener(view1 -> startOCRFromShareMenu(la[2], imageUri)); } - private String getLanguageNameFromCode(String code) { - return languagesNames.get(languagesCodes.indexOf(code)); + private String getLanguageNameFromCode(String code, boolean multipleLanguages) { + return multipleLanguages ? code : languagesNames.get(languagesCodes.indexOf(code)); } @Override protected void onResume() { super.onResume(); - mLanguageName.setText(getLanguageNameFromCode(mLanguage)); + mLanguageName.setText(getLanguageNameFromCode(mLanguage,true)); } public void startOCRFromShareMenu(String lang, Uri imageUri) { @@ -349,7 +351,7 @@ private void downloadLanguageData(final String dataType, final String lang) { if (ni != null && ni.isConnected()) { //region show confirmation dialog, On 'yes' download the training data. - String msg = String.format(getString(R.string.download_description), getLanguageNameFromCode(lang)); + String msg = String.format(getString(R.string.download_description), getLanguageNameFromCode(lang, true)); dialog = new AlertDialog.Builder(this) .setTitle(R.string.training_data_missing) .setCancelable(false) @@ -525,8 +527,18 @@ public void onProgressValues(final TessBaseAPI.ProgressValues progressValues) { public void saveBitmapToStorage(Bitmap bitmap) { FileOutputStream fileOutputStream; + File dir; + File file; try { - fileOutputStream = openFileOutput("last_file.jpeg", Context.MODE_PRIVATE); +// openFileOutput(..) will open file: /data/user/0/${packageName}/files/last_file.jpeg +// App data in "/storage/emulated/0/Android/data" on Android R + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + dir = CropFileProvider.filesDir(getApplicationContext()); + file = new File(dir, "last_file.jpeg"); + fileOutputStream = new FileOutputStream(file); + } else { + fileOutputStream = openFileOutput("last_file.jpeg", Context.MODE_PRIVATE); + } bitmap.compress(Bitmap.CompressFormat.JPEG, 30, fileOutputStream); fileOutputStream.close(); } catch (FileNotFoundException e) { @@ -541,8 +553,18 @@ public void saveBitmapToStorage(Bitmap bitmap) { public Bitmap loadBitmapFromStorage() { Bitmap bitmap = null; FileInputStream fileInputStream; + File dir; + File file; try { - fileInputStream = openFileInput("last_file.jpeg"); +// openFileInput(..) will open file: /data/user/0/${packageName}/files/last_file.jpeg +// App data in "/storage/emulated/0/Android/data" on Android R + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + dir = CropFileProvider.filesDir(getApplicationContext()); + file = new File(dir, "last_file.jpeg"); + fileInputStream = new FileInputStream(file); + } else { + fileInputStream = openFileInput("last_file.jpeg");; + } bitmap = BitmapFactory.decodeStream(fileInputStream); fileInputStream.close(); diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/ActivityConfigTasker.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/ActivityConfigTasker.kt new file mode 100644 index 00000000..948397ea --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/ActivityConfigTasker.kt @@ -0,0 +1,57 @@ +package io.github.subhamtyagi.ocr.tasker + +import android.app.Activity +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import androidx.viewbinding.ViewBinding +import com.joaomgcd.taskerpluginlibrary.SimpleResultError +import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig +import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelper +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner +abstract class ActivityConfigTasker, THelper : TaskerPluginConfigHelper, TBinding : ViewBinding> : Activity(), TaskerPluginConfig { + abstract fun getNewHelper(config: TaskerPluginConfig): THelper + protected abstract fun inflateBinding(layoutInflater: LayoutInflater): TBinding? + + protected var binding: TBinding? = null + + protected val taskerHelper by lazy { getNewHelper(this) } + + open val isConfigurable = true + override val context get() = applicationContext + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = inflateBinding(layoutInflater) + if (!isConfigurable) { + taskerHelper.finishForTasker() + return + } + binding?.root?.let { setContentView(it) } + taskerHelper.onCreate() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + return if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) { + val result = taskerHelper.onBackPressed() + if (result is SimpleResultError) { + alert("Warning", "Settings are not valid:\n\n${result.message}") + } + return result.success + } else super.onKeyDown(keyCode, event) + } + + override fun onBackPressed() { + } +} + + +abstract class ActivityConfigTaskerNoOutput, THelper : TaskerPluginConfigHelper, TBinding : ViewBinding> : ActivityConfigTasker() +abstract class ActivityConfigTaskerNoInput, THelper : TaskerPluginConfigHelper> : ActivityConfigTasker() { + override fun assignFromInput(input: TaskerInput) {} + override val inputForTasker = TaskerInput(Unit) + override fun inflateBinding(layoutInflater: LayoutInflater) = null + override val isConfigurable = false +} + +abstract class ActivityConfigTaskerNoOutputOrInput, THelper : TaskerPluginConfigHelper> : ActivityConfigTaskerNoInput() \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt new file mode 100644 index 00000000..f84aae82 --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt @@ -0,0 +1,25 @@ +package io.github.subhamtyagi.ocr.tasker; + +import android.content.Context +import com.googlecode.tesseract.android.TessBaseAPI +import io.github.subhamtyagi.ocr.utils.SpUtil +import io.github.subhamtyagi.ocr.utils.Utils +import java.io.File + +interface OCRFactory { + fun createAPI(): Any +} + +class TessBaseOCRFactory(private val context: Context) : OCRFactory{ + override fun createAPI(): TessBaseAPI { + SpUtil.getInstance().init(context) + val mLanguage = Utils.getTrainingDataLanguage() + val mPageSegMode = Utils.getPageSegMode() + val dir = File(context.getExternalFilesDir(Utils.getTrainingDataType().toString())!!.absolutePath) + val mEngineMode = Utils.getEngineMode(); + val api = TessBaseAPI() + api.init(dir.absolutePath, mLanguage, mEngineMode) + api.pageSegMode = mPageSegMode + return api + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt new file mode 100644 index 00000000..06a0f25c --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt @@ -0,0 +1,107 @@ +package io.github.subhamtyagi.ocr.tasker + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import androidx.core.content.FileProvider +import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig +import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelper +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.theartofdev.edmodo.cropper.BitmapUtils +import com.theartofdev.edmodo.cropper.CropFileProvider +import io.github.subhamtyagi.ocr.databinding.ActivityConfigOcrBinding +import io.github.subhamtyagi.ocr.tasker.ChannelManager.channelResult +import io.github.subhamtyagi.ocr.utils.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + + +class BackgroundWork: CoroutineScope by MainScope() { + suspend fun readText(context: Context, imagePathName: String) { + launch { + // start OCR + // Use the withContext function to switch context. The withContext function + // can switch to the specified scheduler inside the coroutine. + withContext(IO) { + val result = readTextFromOCRAPI(context, imagePathName) + channelResult.send(result) + } + } + } + private fun readTextFromOCRAPI(context: Context, pathName: String): String { + pathName.ifEmpty { return "Scan Failed: Must choose image file!" } + val imageFile = File(pathName) + val imageUri = FileProvider.getUriForFile(context, CropFileProvider.authority(context), imageFile) + var bitmap = BitmapUtils.decodeSampledBitMap(context, imageUri)?.bitmap + + // call image reader api + val result = TessBaseOCRFactory(context).let { + val api = it.createAPI() + if (Utils.isPreProcessImage()) bitmap = Utils.preProcessBitmap(bitmap) + api.setImage(bitmap) + val textOnImage = try { + api.utF8Text; + } catch (e: Exception) { + "Scan Failed: WTF: Must be reported to developer!" + } + textOnImage.ifEmpty { + "Scan Failed: Couldn't read the image\nProblem may be related to Tesseract or no Text on Image!" + } + } + return result + } + +} + +class BackgroundHelper(config: TaskerPluginConfig): TaskerPluginConfigHelper(config){ + override val inputClass get() = OCRInput::class.java + override val outputClass get() = OCROutput::class.java + override val runnerClass = OCRRunner::class.java + + override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { + blurbBuilder.append("\nConfigure some variables to perform actions ") + } +} + +class ActivityConfigOCR : ActivityConfigTasker() { + + private val taskHelper by lazy {BackgroundHelper(this)} + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding?.editImagePathName?.setOnClickListener { showVariableDialog() } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + finishForTasker() + } + + private fun showVariableDialog() { + val relevantVariables = taskerHelper.relevantVariables.toList() + if (relevantVariables.isEmpty()) return "No variables to select.\n\nCreate some local variables in Tasker to show here.".toToast(this) + selectOne("Select a Tasker variable", relevantVariables) { binding?.editImagePathName?.setText(it) } + } + private fun finishForTasker() { + taskHelper.finishForTasker(); + } + + override fun getNewHelper(config: TaskerPluginConfig) = BackgroundHelper(config) + + override fun inflateBinding(layoutInflater: LayoutInflater) = ActivityConfigOcrBinding.inflate(layoutInflater) + + override val inputForTasker get() = TaskerInput(OCRInput(binding?.editImagePathName?.text?.toString(),binding?.editResultVariableName?.text?.toString())) + + override fun assignFromInput(input: TaskerInput) { + input.regular.run { + binding?.editImagePathName?.setText(imagePathName) + binding?.editResultVariableName?.setText(resultVariableName) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRInput.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRInput.kt new file mode 100644 index 00000000..d1c903b5 --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRInput.kt @@ -0,0 +1,30 @@ +package io.github.subhamtyagi.ocr.tasker + +import com.joaomgcd.taskerpluginlibrary.input.TaskerInputField +import com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot + +@TaskerInputRoot +class OCRInput @JvmOverloads constructor( + @field:TaskerInputField("imagePathName", labelResIdName = "image_path_name") + var imagePathName: String?= null, + + @field:TaskerInputField("resultVariableName", labelResIdName = "result_variable_name") + var resultVariableName: String?= null) +/** + * list of infos that could come from a main app. Are not related to the Tasker UI. + * In this list each item has a property that says if it's a Tasker value or not + */ +class InfoFromMainApp(val name: String, val hasTaskerVariable: Boolean = false) +class InfosFromMainApp : ArrayList() + +val infos = InfosFromMainApp().apply { + addAll(arrayOf( + InfoFromMainApp("image_path_name", true), + InfoFromMainApp("genre", false) + )) +} + +/** + * Get all infos that are Tasker values + */ +val infosForTasker get() = infos.filter { it.hasTaskerVariable } \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt new file mode 100644 index 00000000..52d09795 --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt @@ -0,0 +1,13 @@ +package io.github.subhamtyagi.ocr.tasker + +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable + +@TaskerOutputObject() +class OCROutput( + @get:TaskerOutputVariable(VAR_RESULT, labelResIdName = "result", htmlLabelResIdName = "result_description") var result: String? +) { + companion object { + const val VAR_RESULT = "result" + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt new file mode 100644 index 00000000..b298a17d --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt @@ -0,0 +1,37 @@ +package io.github.subhamtyagi.ocr.tasker + +import android.content.Context +import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerAction +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.runner.TaskerOutputRename +import com.joaomgcd.taskerpluginlibrary.runner.TaskerOutputRenames +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess +import io.github.subhamtyagi.ocr.R +import io.github.subhamtyagi.ocr.tasker.ChannelManager.channelResult +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.runBlocking + +object ChannelManager { + val channelResult = Channel() +} +const val TAG = "OCRRunner" +class OCRRunner : TaskerPluginRunnerAction() { + + override val notificationProperties get() = NotificationProperties(iconResId = R.drawable.plugin) + override fun run(context: Context,input: TaskerInput): TaskerPluginResult { + val imagePathName = input.regular.imagePathName?: "" + val output = runBlocking { + BackgroundWork().readText(context, imagePathName); + val result = channelResult.receive() + OCROutput(result) + } + return TaskerPluginResultSucess(output) + } + + override fun addOutputVariableRenames(context: Context,input: TaskerInput,renames: TaskerOutputRenames) { + super.addOutputVariableRenames(context, input, renames) + renames.add(TaskerOutputRename(OCROutput.VAR_RESULT, input.regular.resultVariableName)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/Util.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/Util.kt new file mode 100644 index 00000000..0410c97f --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/Util.kt @@ -0,0 +1,43 @@ +package io.github.subhamtyagi.ocr.tasker + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.M +import android.os.Handler +import android.os.Looper +import android.widget.ArrayAdapter +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Toast +import io.github.subhamtyagi.ocr.R + +fun String.toToast(context: Context) { + Handler(Looper.getMainLooper()).post { Toast.makeText(context, this, Toast.LENGTH_LONG).show() } +} + +fun Activity.selectOne(title: String, options: List, callback: (String?) -> Unit) { + AlertDialog.Builder(this).apply { + setIcon(R.mipmap.ic_launcher) + setTitle(title) + val arrayAdapter = ArrayAdapter(this@selectOne, android.R.layout.select_dialog_singlechoice).apply { + addAll(options) + } + setAdapter(arrayAdapter) { _, which -> callback(arrayAdapter.getItem(which)) } + setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss(); callback(null) } + }.show() +} + +fun Activity.alert(title: String, message: String) { + val alertDialog = AlertDialog.Builder(this).create() + alertDialog.setTitle(title) + alertDialog.setMessage(message) + alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK") { dialog, _ -> dialog.dismiss() } + alertDialog.show() +} + +val RadioGroup.checkedRadioButton get() = this.findViewById(checkedRadioButtonId) + +val Context.canDrawOverlays get() = if (SDK_INT < M) true else android.provider.Settings.canDrawOverlays(this) + diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java index 0dc3e668..f06506e1 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java @@ -33,6 +33,7 @@ public class Constants { public static final String KEY_OTSU_THRESHOLD = "otsu_threshold"; public static final String KEY_FIND_SKEW_AND_DESKEW = "deskew_img"; public static final String KEY_PAGE_SEG_MODE = "key_ocr_psm_mode"; + public static final String KEY_ENGINE_MODE = "key_oem_mode"; } diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Utils.java b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Utils.java index 72ddfc99..0b9db917 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Utils.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Utils.java @@ -18,6 +18,8 @@ public class Utils { private static final String DEFAULT_LANGUAGE = "eng"; + private static final Set DEFAULT_MULTIPLE_LANGUAGE = Set.of("chi_sim", "eng"); + private static final String DEFAULT_ENGINE_MODE = "3"; @SuppressLint("DefaultLocale") public static String getSize(int size) { @@ -67,7 +69,7 @@ public static boolean isPersistData() { } public static String getTesseractStringForMultipleLanguages(Set langs) { - if (langs == null) return DEFAULT_LANGUAGE; + if (langs == null || langs.size() == 0) return DEFAULT_LANGUAGE; StringBuilder rLanguage = new StringBuilder(); for (String lang : langs) { rLanguage.append(lang); @@ -82,7 +84,7 @@ public static String getTrainingDataType() { public static String getTrainingDataLanguage() { if (SpUtil.getInstance().getBoolean(Constants.KEY_ENABLE_MULTI_LANG)) { - return getTesseractStringForMultipleLanguages(SpUtil.getInstance().getStringSet(Constants.KEY_LANGUAGE_FOR_TESSERACT_MULTI, null)); + return getTesseractStringForMultipleLanguages(SpUtil.getInstance().getStringSet(Constants.KEY_LANGUAGE_FOR_TESSERACT_MULTI, DEFAULT_MULTIPLE_LANGUAGE)); } else { return SpUtil.getInstance().getString(Constants.KEY_LANGUAGE_FOR_TESSERACT, DEFAULT_LANGUAGE); } @@ -102,6 +104,10 @@ public static int getPageSegMode() { return Integer.parseInt(SpUtil.getInstance().getString(Constants.KEY_PAGE_SEG_MODE, "1")); } + public static int getEngineMode(){ + return Integer.parseInt(SpUtil.getInstance().getString(Constants.KEY_ENGINE_MODE, DEFAULT_ENGINE_MODE)); + } + public static void putLastUsedText(String text) { SpUtil.getInstance().putString(Constants.KEY_LAST_USE_IMAGE_TEXT, text); } diff --git a/app/src/main/res/drawable/ic_baseline_engine_mode_32.xml b/app/src/main/res/drawable/ic_baseline_engine_mode_32.xml new file mode 100644 index 00000000..8409ac10 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_engine_mode_32.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07959f91 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/plugin.png b/app/src/main/res/drawable/plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..66385d12a0a52b7b03f38c0a215b7efe10fd034d GIT binary patch literal 863 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3c4$CrY$FeEe8*n>`W}9Z-A`)1 zyl#1_vS;kK)AARt@86rg{}1D1QT9J6^|qPK z%v(M&2S@!j=CAy};>pGp?hM9EGXxLxFr={^@)dY{9qHOg!9jA8a4bob=mr z{@%-fS24tM$$hYVJhS=Mdae%(A27-r&(qxbKlA>#)%RF!J3D4~R9yM+<9&yPw56|IWE8QNXVtty~k$XobOq6 z`P$#3g6CChZzt3!yu2Cq`1V_V)x9~t|2^tBKIxv~BA1jh@xsYnz2Hv_U}w462jzn-!hY9;Dou2n;yY64!{5l*E!$ ztK_0oAjM#0U}&LhV61Co6k=#%Wny7vXrygmU}a!1t@iO_6b-rgDVb@NxHUXih>izp z&;Z#`T$XN?lvtdqTUwOKkd~Q~YNfBQUy@s(pPQJTnVhX_Xki%cZ;)lgSHyk?sE5JR L)z4*}Q$iB}sLoC} literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_config_ocr.xml b/app/src/main/res/layout/activity_config_ocr.xml new file mode 100644 index 00000000..6f5a4cc0 --- /dev/null +++ b/app/src/main/res/layout/activity_config_ocr.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8fa1d9fe..e1d5508e 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -58,4 +58,5 @@ ما لغة هذه الصورة؟ اسم اللغة حدد أو ابحث عن اللغات + يحدد الدقة والكفاءة \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index bfd6852e..d9d6b0a8 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -58,4 +58,5 @@ Выраўноўванне выявы Дадатковыя налады Паляпшэнне выявы + Вызначае дакладнасць і эфектыўнасць \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index af0690ad..506ee54e 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -14,4 +14,5 @@ ছবি আগে প্রক্রিয়া করো টেসাররক্ট তথ্য অন্য পছন্দসমূহ + Determines accuracy and efficiency \ No newline at end of file diff --git a/app/src/main/res/values-cn/strings.xml b/app/src/main/res/values-cn/strings.xml index f2e003bb..adf9deb6 100644 --- a/app/src/main/res/values-cn/strings.xml +++ b/app/src/main/res/values-cn/strings.xml @@ -59,4 +59,10 @@ 此图中的文字是什么语言? 语言名 选择或搜索语言 + 决定功能和速度 + 文字识别结果 + 应用存储空间下的图片绝对路径 + 存储文字识别结果的可选变量名 + 文字识别结果变量名 + 图片路径名 \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 09a54084..e9e95a4f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -58,4 +58,5 @@ Jaký jazyk je na tomto obrázku\? Název jazyka Vybrat nebo hledat jazyky + Určuje přesnost a efektivitu \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2989cd9a..51827d0f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -58,4 +58,5 @@ Ausgewählte Sprache: Welche Sprache hat dieses Bild\? Name der Sprache + Bestimmt Genauigkeit und Effizienz \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 684a1068..d4f2efbb 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -58,4 +58,5 @@ Umbral de OTSU Funciones para el tratamiento de las imágenes Dirige cómo Tesseract divide la imagen en líneas de texto y palabras. + Determina la precisión y la eficiencia. \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 522d7eeb..4df16cde 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -58,4 +58,5 @@ وظایف مربوط به پردازش تصویر انحراف را پیدا کرده و تصویر را اصلاح نمایید انجام باینری‌سازی آستانه Otsu تطبیقی محلی تصاویر + دقت و کارایی را تعیین می کند \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 87f4149f..c73ec406 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -58,4 +58,5 @@ Quelle est la langue de cette image \? Nom de la langue Sélectionner ou rechercher des langues + Détermine la précision et l’efficacité \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 5c95f027..ef4d576e 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -58,4 +58,5 @@ מיסוך לא מחדד שיפור תמונה עיבוד טרום לתמונה לדיוק משופר + קובע דיוק ויעילות \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index cd327a9a..9fa567cc 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -30,4 +30,5 @@ ताज़ा ऐप छवि पर पाठ खींचेगा छवि पर पाठ ड्रा करें + सटीकता और दक्षता निर्धारित करता है \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 20a4d44b..b7b73f7c 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -58,4 +58,5 @@ Bahasa apa yang dimiliki gambar ini\? Nama bahasa Pilih atau cari bahasa + Menentukan akurasi dan efisiensi \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b9a9a086..f051c383 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -58,4 +58,5 @@ Seleziona più lingue Funzioni di elaborazione dell\'immagine Impostazioni Avanzate Tesseract + Determina l\'accuratezza e l\'efficienza \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index eed15032..480d9c0a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -58,4 +58,5 @@ 이미지 기울기 보정 이미지 향상 이미지 처리 기능 + 정확성과 효율성을 결정합니다. \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index d030c8f9..740c2c5e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -54,4 +54,5 @@ Angir hvordan Tesseract deler bilder i linjer av tekst og ord. Avanserte Tesseract-innstillinger Bildebehandlingsfunksjoner + Bestemmer nøyaktighet og effektivitet \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f0504c94..2954cbbf 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -37,4 +37,5 @@ Broncode Afbeelding Probeert adaptief het contrast uit te breiden naar het volledige dynamische bereik + Bepaalt nauwkeurigheid en efficiëntie \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index de714bdc..4e9075e9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -59,4 +59,5 @@ Wybrany język: Nazwa języka Wybierz lub wyszukaj języki + Określa dokładność i wydajność \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d9745ae5..8f77b648 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -58,4 +58,5 @@ Desenhar texto na imagem Modo de segmentação de página Não há dados de treinamento! + Determina precisão e eficiência \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index f19099c7..4d1ddeea 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -58,4 +58,5 @@ Nome do idioma Selecionar ou pesquisar idiomas Idioma selecionado: + Determina precisão e eficiência \ No newline at end of file diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 4eb1b686..b12adbb4 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -19,4 +19,5 @@ අනුරුව බාගැනෙමින්… අවසාන ප්‍රතිඵලය පෙන්වන්න + නිරවද්යතාව සහ කාර්යක්ෂමතාව තීරණය කරයි \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index a6b3daec..566fb518 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + துல்லியம் மற்றும் செயல்திறனை தீர்மானிக்கிறது + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b9327391..9c47c97b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -58,4 +58,5 @@ Seçilen Dil: Bu resmin dili ne\? Dil Adı + Doğruluğu ve verimliliği belirler \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cd34cde0..a2363671 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -58,4 +58,5 @@ Якою мовою є це зображення\? Назва мови Вибір або пошук мов + Визначає точність і ефективність \ No newline at end of file diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index c83807c1..592278d6 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -43,4 +43,5 @@ ⵓⵔ ⵉⵣⵔⵉ ⵢⵉⴽⵉⵣ ⵏ ⴽⵔⴰ ⵏ ⵓⴹⵕⵉⵚ. ⵃⴹⵓ ⵜⴰⵡⵍⴰⴼⵜ ⴷ ⵓⴹⵕⵉⵚ ⴷⴼⴼⵉⵔ ⵏ ⵡⵓⴼⵓⵖ ⵙⴳ ⵜⵙⵏⵙⵉ ⵓⵔ ⵍⵍⵉⵏⵜ ⵜⵎⵓⵛⴰ ⵏ ⵓⵙⴰⵏⵓⵏ! + Determines accuracy and efficiency \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d0498cf2..f4e5621a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -58,4 +58,5 @@ 這圖像有什麼類言? 語言名稱 選擇/搜尋語言 + 決定準確性和效率 \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index f34e7b60..52aa451a 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -312,5 +312,17 @@ 12 13 + + OEM_TESSERACT_ONLY + OEM_LSTM_ONLY + OEM_TESSERACT_LSTM_COMBINED + OEM_DEFAULT + + + 0 + 1 + 2 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 45ac2e0c..a33c582e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,4 +73,11 @@ What language this image have? Language Name Select or Search Languages + Image path name + Result variable name + Optional variable name that stores image text + OCR text result + Determines accuracy and efficiency + OCR Engine mode + The absolute path of the image in the application storage space \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 492da9e4..cb4645a2 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -67,6 +67,15 @@ android:summary="@string/psm_summary" android:title="@string/segmentation_mode" app:icon="@drawable/ic_baseline_page_seg_32" /> + + diff --git a/build.gradle b/build.gradle index 7937b058..8b9eaec1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,17 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.9.20' + ext.java_version = '17' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' - +// classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.1.4' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/cropper/build.gradle b/cropper/build.gradle index a0559576..fb0b5a3d 100644 --- a/cropper/build.gradle +++ b/cropper/build.gradle @@ -2,17 +2,22 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 33 + compileSdkVersion 34 defaultConfig { minSdkVersion 14 - targetSdkVersion 33 + targetSdkVersion 34 } + compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility "$java_version" + targetCompatibility "$java_version" + } + namespace 'com.theartofdev.edmodo.cropper' + buildFeatures { + viewBinding true } - lintOptions { + lint { abortOnError false } } diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java index 1f9fa218..67b0c1c9 100644 --- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java +++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java @@ -43,7 +43,7 @@ /** * Utility class that deals with operations with an ImageView. */ -final class BitmapUtils { +public final class BitmapUtils { static final Rect EMPTY_RECT = new Rect(); @@ -150,6 +150,27 @@ static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, } } + public static BitmapSampled decodeSampledBitMap(Context context, Uri uri){ + // Get ContentResolver + ContentResolver resolver = context.getContentResolver(); + + // Get height and width of image through BitmapFactory.Options inJustDecodeBounds + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + + try (InputStream stream = resolver.openInputStream(uri)) { + // Just get height and width of image, not load into memory + BitmapFactory.decodeStream(stream, null, options); + } catch (IOException e) { + e.printStackTrace(); + } + + int imageWidth = options.outWidth; + int imageHeight = options.outHeight; + + return decodeSampledBitmap(context, uri, imageWidth, imageHeight); + } + /** * Crop image bitmap from given bitmap using the given points in the original bitmap and the given * rotation.
@@ -880,7 +901,7 @@ private static void closeSafe(Closeable closeable) { /** * Holds bitmap instance and the sample size that the bitmap was loaded/cropped with. */ - static final class BitmapSampled { + public static final class BitmapSampled { /** * The bitmap instance diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropFileProvider.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropFileProvider.java index e9403ade..098e2ff5 100644 --- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropFileProvider.java +++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropFileProvider.java @@ -19,15 +19,15 @@ import java.util.UUID; public class CropFileProvider { - static String authority(Context context) { + public static String authority(Context context) { return String.format("%s.image-cropper.provider", context.getPackageName()); } - static File filesDir(Context context) { + public static File filesDir(Context context) { return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); } - static File file(Context context, String extension) { + public static File file(Context context, String extension) { try { return File.createTempFile(UUID.randomUUID().toString(), extension, CropFileProvider.filesDir(context)); } catch (Exception ignored) { diff --git a/gradle.properties b/gradle.properties index 19767978..9dbc43cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,24 +1,22 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +#Wed Nov 29 13:59:48 CST 2023 +android.defaults.buildfeatures.buildconfig=true +android.enableJetifier=false +android.enableResourceOptimizations=true +android.nonFinalResIds=false +android.nonTransitiveRClass=true android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX - -android.enableJetifier=true -org.gradle.parallel=true org.gradle.deamon=true -android.enableResourceOptimizations=true -android.nonTransitiveRClass=true \ No newline at end of file +org.gradle.jvmargs=-Xmx8192M -Dkotlin.daemon.jvm.options\="-Xmx8192M" -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8 -XX\:+UseParallelGC -XX\:MaxMetaspaceSize\=1g +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4267fee2..5fcbb129 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6da088be..f308e76a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ -include ':app','cropper' +include ':app','cropper',':taskerpluginlibrary' rootProject.name='Characher Recognizer' diff --git a/taskerpluginlibrary/.gitignore b/taskerpluginlibrary/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/taskerpluginlibrary/.gitignore @@ -0,0 +1 @@ +/build diff --git a/taskerpluginlibrary/build.gradle b/taskerpluginlibrary/build.gradle new file mode 100644 index 00000000..37416ce8 --- /dev/null +++ b/taskerpluginlibrary/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +tasks.withType(Javadoc).all { + enabled = false +} +android { + compileSdk 34 + + + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 34 + consumerProguardFiles 'proguard-rules.pro' + } + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + targetCompatibility "$java_version" + sourceCompatibility "$java_version" + } + namespace 'com.joaomgcd.taskerpluginlibrary' + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} +repositories { + mavenCentral() +} +ext { + PUBLISH_GROUP_ID = 'com.joaomgcd' + PUBLISH_VERSION = '0.4.9' + PUBLISH_ARTIFACT_ID = 'taskerpluginlibrary' +} \ No newline at end of file diff --git a/taskerpluginlibrary/proguard-rules.pro b/taskerpluginlibrary/proguard-rules.pro new file mode 100644 index 00000000..7e12baaa --- /dev/null +++ b/taskerpluginlibrary/proguard-rules.pro @@ -0,0 +1,35 @@ + +#-keepattributes SourceFile,LineNumberTable + +-keepattributes *Annotation* +-keep public class com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject { *; } +-keep public class com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable { *; } + +-keepclasseswithmembers class * { + @com.joaomgcd.taskerpluginlibrary.input.TaskerInputField ; +} +-keep @com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot public class * +-keepclassmembers @com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot class * { + public (...); +} +-keep @com.joaomgcd.taskerpluginlibrary.input.TaskerInputObject public class * +-keepclassmembers @com.joaomgcd.taskerpluginlibrary.input.TaskerInputObject class * { + public (...); +} +-keep @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject public class * +-keepclassmembers class * { + @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject *; +} +-keepclassmembers class * { + @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable *; +} +-keepclassmembers @com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject class * { *; } +-keep public class * extends com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner { *; } +-keepclassmembernames class com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig { *; } + +-keep public class net.dinglisch.android.tasker.PluginResultReceiver { *; } + +-dontwarn android.** +-dontwarn com.google.** + +-keep public class kotlin.Unit { *; } \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/AndroidManifest.xml b/taskerpluginlibrary/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f2918d82 --- /dev/null +++ b/taskerpluginlibrary/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Exceptions.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Exceptions.kt new file mode 100644 index 00000000..449a4cb6 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Exceptions.kt @@ -0,0 +1,6 @@ +package com.joaomgcd.taskerpluginlibrary + +/** + * Used to crash app when plugin developer forgets to have an empty constructor in an input class + */ +class NoEmptyConstructorException(inputClassName:String) : RuntimeException("Tasker Input class ${inputClassName} must have empty constructor") \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/SimpleResult.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/SimpleResult.kt new file mode 100644 index 00000000..8ae6a9cf --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/SimpleResult.kt @@ -0,0 +1,31 @@ +package com.joaomgcd.taskerpluginlibrary + +/** + * Convenience class to indicate if a process ended with success or failure. + * If a success, you'll have access to an optional payload by using #SimpleResultSuccessWithPayload + * If an error, you'll have access to an error message by using #SimpleResultError + * + */ +sealed class SimpleResult(val success: Boolean) { + companion object { + fun get(block: () -> R) = try { + SimpleResultSuccessWithPayload(block()) + } catch (t: Throwable) { + SimpleResultError(t) + } + + fun run(block: () -> Unit) = try { + block() + SimpleResultSuccess() + } catch (t: Throwable) { + SimpleResultError(t) + } + } +} + +open class SimpleResultSuccess : SimpleResult(true) +class SimpleResultSuccessWithPayload(val payload: TPayload) : SimpleResultSuccess() + +class SimpleResultError(val message: String) : SimpleResult(false) { + constructor(t: Throwable) : this(t.message ?: t.toString()) +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/TaskerPluginConstants.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/TaskerPluginConstants.kt new file mode 100644 index 00000000..7ad00d9f --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/TaskerPluginConstants.kt @@ -0,0 +1,23 @@ +package com.joaomgcd.taskerpluginlibrary + +object TaskerPluginConstants { + const val RESULT_CONDITION_SATISFIED = 16 + const val RESULT_CONDITION_UNSATISFIED = 17 + const val RESULT_CONDITION_UNKNOWN = 18 + const val ACTION_EDIT_SETTING = "com.twofortyfouram.locale.intent.action.EDIT_SETTING" + const val ACTION_FIRE_SETTING = "com.twofortyfouram.locale.intent.action.FIRE_SETTING" + const val ACTION_EDIT_CONDITION = "com.twofortyfouram.locale.intent.action.EDIT_CONDITION" + const val ACTION_QUERY_CONDITION = "com.twofortyfouram.locale.intent.action.QUERY_CONDITION" + const val ACTION_REQUEST_QUERY = "com.twofortyfouram.locale.intent.action.REQUEST_QUERY" + const val EXTRA_STRING_BREADCRUMB = "com.twofortyfouram.locale.intent.extra.BREADCRUMB" + const val EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB" + const val EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE" + const val EXTRA_ACTIVITY = "com.twofortyfouram.locale.intent.extra.ACTIVITY" + const val VARIABLE_REPLACE_KEYS = "net.dinglisch.android.tasker.extras.VARIABLE_REPLACE_KEYS" + const val VARIABLES = "net.dinglisch.android.tasker.extras.VARIABLES" + const val EXTRA_ACTION_INPUT_CLASS = "net.dinglisch.android.tasker.extras.ACTION_INPUT_CLASS" + const val EXTRA_ACTION_RUNNER_CLASS = "net.dinglisch.android.tasker.extras.ACTION_RUNNER_CLASS" + const val EXTRA_CONDITION_UPDATE_CLASS = "net.dinglisch.android.tasker.extras.CONDITION_UPDATE_CLASS" + const val EXTRA_WAS_CONFIGURED_BEFORE = "net.dinglisch.android.tasker.extras.EXTRA_WAS_CONFIGURED_BEFORE" + const val EXTRA_CAN_BIND_FIRE_SETTING = "net.dinglisch.android.tasker.EXTRA_CAN_BIND_FIRE_SETTING" +} diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Util.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Util.kt new file mode 100644 index 00000000..377022c7 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/Util.kt @@ -0,0 +1,54 @@ +package com.joaomgcd.taskerpluginlibrary + +import android.content.Context + + +/** + * Does a different action for a value, depending on its type. Only Tasker supported types are considered + * + * @param value the value to act on + */ +fun getForTaskerCompatibleInputTypes(value: Any?, + forNull: (Any?) -> TResult, + forString: (String) -> TResult, + forInt: (Int) -> TResult, + forLong: (Long) -> TResult, + forFloat: (Float) -> TResult, + forDouble: (Double) -> TResult, + forBoolean: (Boolean) -> TResult, + forStringArray: (Array) -> TResult, + forStringArrayList: (ArrayList) -> TResult): TResult { + if (value == null) return forNull(value) + return when (value) { + is String -> forString(value) + is Int -> forInt(value) + is Long -> forLong(value) + is Float -> forFloat(value) + is Double -> forDouble(value) + is Boolean -> forBoolean(value) + is Array<*> -> forStringArray(value as Array) + is ArrayList<*> -> forStringArrayList(value as ArrayList) + else -> throw RuntimeException("Tasker doesn't support inputs of type ${value::class.java}") + } +} + +fun Context.getStringResourceId(resourceName: String): Int { + return resources.getIdentifier(resourceName, "string", packageName) +} + +const val STRING_RES_ID_NOT_SET = -1 +const val STRING_RES_ID_NAME_NOT_SET = "" + +fun Context.getStringFromResourceIdOrResourceName(resourceId: Int, resourceName: String, defaultString: String): String { + val resourceIdSet = resourceId != STRING_RES_ID_NOT_SET + val resourceNameIdSet = resourceName != STRING_RES_ID_NAME_NOT_SET + if (!resourceIdSet && !resourceNameIdSet) return defaultString + + val resourceIdFinal = if (resourceNameIdSet) getStringResourceId(resourceName) else resourceId + return try { + getString(resourceIdFinal) + } catch (t: Throwable) { + defaultString + } + +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/ActionReceivers.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/ActionReceivers.kt new file mode 100644 index 00000000..0ebb13d8 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/ActionReceivers.kt @@ -0,0 +1,32 @@ +package com.joaomgcd.taskerpluginlibrary.action + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.joaomgcd.taskerpluginlibrary.extensions.mayNeedToStartForeground +import com.joaomgcd.taskerpluginlibrary.extensions.runFromTasker +import com.joaomgcd.taskerpluginlibrary.runner.IntentServiceParallel +import net.dinglisch.android.tasker.TaskerPlugin + + +class BroadcastReceiverAction : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + resultCode = TaskerPlugin.Setting.RESULT_CODE_PENDING + try { + runFromTasker(context, intent) + } catch (ex: Exception) { + ex.printStackTrace() + } + } +} + +class IntentServiceAction : IntentServiceParallel("IntentServiceTaskerAction") { + override fun onHandleIntent(intent: Intent) { + val mayNeedToStartForeground: Boolean = intent.mayNeedToStartForeground + startForegroundIfNeeded(mayNeedToStartForeground) + val result = TaskerPluginRunnerAction.runFromIntent(this, intent) + if (!result.hasStartedForeground) { + startForegroundIfNeeded(mayNeedToStartForeground) + } + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerAction.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerAction.kt new file mode 100644 index 00000000..18e1394f --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerAction.kt @@ -0,0 +1,52 @@ +package com.joaomgcd.taskerpluginlibrary.action + +import android.app.IntentService +import android.content.Context +import android.content.Intent +import com.joaomgcd.taskerpluginlibrary.extensions.getTaskerInput +import com.joaomgcd.taskerpluginlibrary.extensions.mayNeedToStartForeground +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.output.runner.TaskerOutputForRunner +import com.joaomgcd.taskerpluginlibrary.runner.* +import net.dinglisch.android.tasker.TaskerPlugin + + +abstract class TaskerPluginRunnerAction() : TaskerPluginRunner() { + + private var taskerIntent: Intent? = null + protected val requestedTimeout: Int? get() = TaskerPlugin.Setting.getHintTimeoutMS(taskerIntent?.extras).let { if (it == -1) null else it } + internal fun runWithIntent(context: IntentServiceParallel?, taskerIntent: Intent?): RunnerActionResult { + if (context == null) return RunnerActionResult(false) + if (taskerIntent == null) return RunnerActionResult(false) + startForegroundIfNeeded(context, mayNeedToStartForeground = taskerIntent.mayNeedToStartForeground) + try { + this.taskerIntent = taskerIntent + val input = taskerIntent.getTaskerInput(context, getInputClass(taskerIntent)) + val result = run(context, input) + result.signalFinish(getArgsSignalFinish(context, taskerIntent, input)) + } catch (t: Throwable) { + TaskerPluginResultError(t).signalFinish(getArgsSignalFinish(context, taskerIntent)) + } + return RunnerActionResult(true) + } + + abstract fun run(context: Context, input: TaskerInput): TaskerPluginResult + + companion object { + internal fun runFromIntent(context: IntentServiceParallel?, taskerIntent: Intent?): RunnerActionResult { + if (context == null) return RunnerActionResult(false) + if (taskerIntent == null) return RunnerActionResult(false) + val runner = TaskerPluginRunner.getFromTaskerIntent>(taskerIntent) + if (runner == null) { + TaskerPluginResultError(0, "Couldn't get action runner from intent").signalFinish(ArgsSignalFinish(context, taskerIntent)) + return RunnerActionResult(false) + } + return runner.runWithIntent(context, taskerIntent) + } + } + + fun getArgsSignalFinish(context: Context, taskerIntent: Intent, input: TaskerInput? = null) = ArgsSignalFinish(context, taskerIntent, getRenames(context, input), { output: TaskerOutputForRunner -> shouldAddOutput(context, input, output) }) + + class RunnerActionResult(val hasStartedForeground: Boolean) +} + diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerActionVariants.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerActionVariants.kt new file mode 100644 index 00000000..5eead6ad --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/action/TaskerPluginRunnerActionVariants.kt @@ -0,0 +1,8 @@ +package com.joaomgcd.taskerpluginlibrary.action + +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner + + +abstract class TaskerPluginRunnerActionNoOutput() : TaskerPluginRunnerAction() +abstract class TaskerPluginRunnerActionNoInput() : TaskerPluginRunnerAction() +abstract class TaskerPluginRunnerActionNoOutputOrInput() : TaskerPluginRunnerActionNoOutput() \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/ConditionReceivers.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/ConditionReceivers.kt new file mode 100644 index 00000000..1e850f77 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/ConditionReceivers.kt @@ -0,0 +1,45 @@ +package com.joaomgcd.taskerpluginlibrary.condition + +import android.app.IntentService +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.joaomgcd.taskerpluginlibrary.TaskerPluginConstants +import com.joaomgcd.taskerpluginlibrary.extensions.startForegroundIfNeeded +import com.joaomgcd.taskerpluginlibrary.runner.IntentServiceParallel +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner +import net.dinglisch.android.tasker.TaskerPlugin + +private fun getAndHandleResult(context: Context?, intent: Intent?, resultBundle: Bundle, handler: (Int, Bundle?) -> Unit) { + if (context == null) return + val resultFromIntent = TaskerPluginRunnerCondition.getResultFromIntent(context, intent) + if (context is IntentService && resultFromIntent?.hasStartedForeground != true) { + TaskerPluginRunner.startForegroundIfNeeded(context) + } + var resultCode = TaskerPluginConstants.RESULT_CONDITION_UNKNOWN + if (resultFromIntent != null) { + resultCode = resultFromIntent.code + resultBundle.putBundle(TaskerPluginConstants.VARIABLES, resultFromIntent.bundle) + } + handler(resultCode, resultBundle) +} + +class BroadcastReceiverCondition : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + getAndHandleResult(context, intent, getResultExtras(true)) { resultCode, _ -> + this.resultCode = resultCode + } + } +} + +class IntentServiceCondition : IntentServiceParallel("IntentServiceTaskerCondition") { + override fun onHandleIntent(intent: Intent) { + startForegroundIfNeeded() + val receiver = TaskerPlugin.Condition.getResultReceiver(intent) ?: return + getAndHandleResult(this, intent, Bundle()) { resultCode, bundle -> + receiver.send(resultCode, bundle) + } + + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginConditionResult.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginConditionResult.kt new file mode 100644 index 00000000..0a47ce6a --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginConditionResult.kt @@ -0,0 +1,6 @@ +package com.joaomgcd.taskerpluginlibrary.condition + +import android.os.Bundle + + +class TaskerPluginConditionResult(val code: Int, val bundle: Bundle?,val hasStartedForeground:Boolean) \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerCondition.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerCondition.kt new file mode 100644 index 00000000..93ffa481 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerCondition.kt @@ -0,0 +1,145 @@ +package com.joaomgcd.taskerpluginlibrary.condition + +import android.app.Activity +import android.app.IntentService +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import com.joaomgcd.taskerpluginlibrary.NoEmptyConstructorException +import com.joaomgcd.taskerpluginlibrary.TaskerPluginConstants +import com.joaomgcd.taskerpluginlibrary.extensions.getTaskerInput +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.input.TaskerInputInfos +import com.joaomgcd.taskerpluginlibrary.output.runner.TaskerOutputForRunner +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultCondition +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionSatisfied +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionUnsatisfied +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner +import net.dinglisch.android.tasker.TaskerPlugin + + +abstract class TaskerPluginRunnerConditionEvent() : TaskerPluginRunnerCondition() { + override val isEvent = true +} + +abstract class TaskerPluginRunnerConditionState() : TaskerPluginRunnerCondition() { + override val isEvent = false +} + +abstract class TaskerPluginRunnerCondition() : TaskerPluginRunner() { + protected abstract val isEvent: Boolean + private fun TaskerPluginResultCondition.getConditionResult(hasStartedForeground: Boolean, input: TaskerInput? = null): TaskerPluginConditionResult { + val bundle = if (this is TaskerPluginResultConditionSatisfied) { + this.getOutputBundle(getRenames(context, input), { output: TaskerOutputForRunner -> shouldAddOutput(context, input, output) }) + } else { + null + } + return TaskerPluginConditionResult(this.conditionResultCode, bundle, hasStartedForeground) + } + + internal fun getResultFromIntent(context: Context?, taskerIntent: Intent?): TaskerPluginConditionResult { + var hasStartedForeground = false + try { + if (context == null || taskerIntent == null) return TaskerPluginResultConditionUnsatisfied().getConditionResult(false) + if (isEvent) { + TaskerPlugin.Event.retrievePassThroughMessageID(taskerIntent).let { if (it == -1) return TaskerPluginResultConditionUnsatisfied().getConditionResult(false) } + } + if (context is IntentService) { + context.startForegroundIfNeeded() + hasStartedForeground = true + } + val input = taskerIntent.getTaskerInput(context, getInputClass(taskerIntent)) + val update = getUpdate(context, taskerIntent) + val satisfiedCondition = getSatisfiedCondition(context, input, update) + return satisfiedCondition.getConditionResult(hasStartedForeground, input) + } catch (t: Throwable) { + t.printStackTrace() + return TaskerPluginResultConditionUnsatisfied().getConditionResult(hasStartedForeground) + } + } + + abstract fun getSatisfiedCondition(context: Context, input: TaskerInput, update: TUpdate?): TaskerPluginResultCondition + + private fun getUpdate(context: Context, taskerIntent: Intent): TUpdate? { + val bundle = TaskerPlugin.Event.retrievePassThroughData(taskerIntent) ?: return null + val updateClass = bundle.getString(TaskerPluginConstants.EXTRA_CONDITION_UPDATE_CLASS) + ?: return null + return try { + val clazz = Class.forName(updateClass) + val update = clazz?.newInstance() as TUpdate + if (update === Unit) return null + TaskerInputInfos.fromBundle(context, update, bundle) + update + } catch (t: InstantiationException) { + throw NoEmptyConstructorException(updateClass) + } catch (t: Throwable) { + t.printStackTrace() + null + } + } + + companion object { + internal fun getResultFromIntent(context: Context?, taskerIntent: Intent?) = TaskerPluginRunner.getFromTaskerIntent>(taskerIntent)?.getResultFromIntent(context, taskerIntent) + + fun requestQuery(context: Context, configActivityClass: Class, update: Any? = null) { + val intentRequest = Intent(TaskerPluginConstants.ACTION_REQUEST_QUERY).apply { + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + putExtra(TaskerPluginConstants.EXTRA_ACTIVITY, configActivityClass.name) + TaskerPlugin.Event.addPassThroughMessageID(this) + update.getUpdateBundle(context)?.let { TaskerPlugin.Event.addPassThroughData(this, it) } + } + val packagesAlreadyHandled = try { + requestQueryThroughServicesAndGetSuccessPackages(context, intentRequest) + } catch (ex: Exception) { + listOf() + } + requestQueryThroughBroadcasts(context, intentRequest, packagesAlreadyHandled) + + } + + private fun requestQueryThroughServicesAndGetSuccessPackages(context: Context, intentRequest: Intent): List { + val packageManager = context.getPackageManager() + val intent = Intent(TaskerPluginConstants.ACTION_REQUEST_QUERY) + val resolveInfos = packageManager.queryIntentServices(intent, 0) + val result = arrayListOf() + resolveInfos.forEach { resolveInfo -> + val serviceInfo = resolveInfo.serviceInfo + val applicationInfo = serviceInfo.applicationInfo + val componentName = ComponentName(serviceInfo.packageName, serviceInfo.name) + intentRequest.component = componentName + try{ + context.startService(intentRequest) + result.add(applicationInfo.packageName) + }catch (t:Throwable){ + //not successful. Don't add to successes + } + } + return result + } + + private fun requestQueryThroughBroadcasts(context: Context, intentRequest: Intent, ignorePackages: List) { + if (ignorePackages.isEmpty()) { + context.sendBroadcast(intentRequest) + return + } + val packageManager = context.getPackageManager() + val intent = Intent(TaskerPluginConstants.ACTION_REQUEST_QUERY) + val resolveInfos = packageManager.queryBroadcastReceivers(intent, 0) + return resolveInfos.forEach { resolveInfo -> + val broadcastInfo = resolveInfo.activityInfo + val applicationInfo = broadcastInfo.applicationInfo + if (ignorePackages.contains(applicationInfo.packageName)) return@forEach + + val componentName = ComponentName(broadcastInfo.packageName, broadcastInfo.name) + intentRequest.component = componentName + context.sendBroadcast(intentRequest) + } + } + + private fun Any?.getUpdateBundle(context: Context) = this?.let { update -> + val bundle = TaskerInputInfos.fromInput(context, update).bundle + bundle.putString(TaskerPluginConstants.EXTRA_CONDITION_UPDATE_CLASS, update::class.java.name) + bundle + } + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerConditionVariants.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerConditionVariants.kt new file mode 100644 index 00000000..00148556 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/condition/TaskerPluginRunnerConditionVariants.kt @@ -0,0 +1,25 @@ +package com.joaomgcd.taskerpluginlibrary.condition + +import android.content.Context +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultCondition +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionSatisfied + + +abstract class TaskerPluginRunnerConditionNoOutput() : TaskerPluginRunnerCondition() +abstract class TaskerPluginRunnerConditionNoInput() : TaskerPluginRunnerCondition() +abstract class TaskerPluginRunnerConditionNoOutputOrInput() : TaskerPluginRunnerConditionNoOutput() +abstract class TaskerPluginRunnerConditionNoOutputOrInputOrUpdate() : TaskerPluginRunnerConditionNoOutputOrInput() + +class TaskerPluginRunnerConditionNoOutputOrInputOrUpdateEvent() : TaskerPluginRunnerConditionNoOutputOrInput() { + override val isEvent: Boolean get() = true + + override fun getSatisfiedCondition(context: Context, input: TaskerInput, update: Unit?): TaskerPluginResultCondition { + return TaskerPluginResultConditionSatisfied(context) + } +} + +abstract class TaskerPluginRunnerConditionNoOutputOrInputOrUpdateState() : TaskerPluginRunnerConditionNoOutputOrInput() { + override val isEvent: Boolean + get() = false +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/HostCapabilities.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/HostCapabilities.kt new file mode 100644 index 00000000..5c45b596 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/HostCapabilities.kt @@ -0,0 +1,28 @@ +package com.joaomgcd.taskerpluginlibrary.config + +import android.os.Bundle +import net.dinglisch.android.tasker.TaskerPlugin + + + +class HostCapabilities(bundleExtras: Bundle?) { + val supportsJsonKeys = TaskerPlugin.hostSupportsKeyEncoding(bundleExtras, TaskerPlugin.Encoding.JSON) + val sendsRelevanVariables = TaskerPlugin.hostSupportsRelevantVariables(bundleExtras) + val action = HostCapabilitesAction(bundleExtras) + val condition = HostCapabilitesCondition(bundleExtras) + val event = HostCapabilitesEvent(bundleExtras) +} + +class HostCapabilitesAction(bundleExtras: Bundle?) { + val canReturnVariables = TaskerPlugin.Setting.hostSupportsVariableReturn(bundleExtras) + val canReplaceVariables = TaskerPlugin.Setting.hostSupportsOnFireVariableReplacement(bundleExtras) + val canRunSynchronously = TaskerPlugin.Setting.hostSupportsSynchronousExecution(bundleExtras) +} + +class HostCapabilitesCondition(bundleExtras: Bundle?) { + val canReturnVariables = TaskerPlugin.Condition.hostSupportsVariableReturn(bundleExtras) +} + +class HostCapabilitesEvent(bundleExtras: Bundle?) { + val supportsPassThroughData = TaskerPlugin.Event.hostSupportsRequestQueryDataPassThrough(bundleExtras) +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfig.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfig.kt new file mode 100644 index 00000000..e455b20b --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfig.kt @@ -0,0 +1,21 @@ +package com.joaomgcd.taskerpluginlibrary.config + +import android.content.Context +import android.content.Intent +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput + + + +interface TaskerPluginConfig { + val context: Context + fun finish() + fun getIntent(): Intent? + fun setResult(resultCode: Int, data: Intent) + fun assignFromInput(input: TaskerInput) + val inputForTasker: TaskerInput +} + +interface TaskerPluginConfigNoInput : TaskerPluginConfig{ + override fun assignFromInput(input: TaskerInput) {} + override val inputForTasker get() = TaskerInput(Unit) +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelper.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelper.kt new file mode 100644 index 00000000..9f186425 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelper.kt @@ -0,0 +1,97 @@ +package com.joaomgcd.taskerpluginlibrary.config + +import android.app.Activity +import android.content.Intent +import com.joaomgcd.taskerpluginlibrary.* +import com.joaomgcd.taskerpluginlibrary.extensions.* +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.input.TaskerInputInfos +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputForConfig +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputsForConfig +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner +import net.dinglisch.android.tasker.TaskerPlugin + +abstract class TaskerPluginConfigHelper>(val config: TaskerPluginConfig) { + + abstract val inputClass: Class + abstract val runnerClass: Class + abstract val outputClass: Class + open val timeoutSeconds: Int = 60 + open val defaultInput: TInput? = null + open fun isInputValid(input: TaskerInput): SimpleResult = SimpleResultSuccess() + + protected val context by lazy { config.context } + private val runner by lazy { runnerClass.newInstance() } + private val taskerIntent = config.getIntent() + val hostCapabilities = HostCapabilities(taskerIntent?.extras) + val relevantVariables: Array = TaskerPlugin.getRelevantVariableList(taskerIntent?.extras) + val breadCrumbs = taskerIntent?.getStringExtra(TaskerPluginConstants.EXTRA_STRING_BREADCRUMB) + + private fun getInputInfos(input: TaskerInput) = TaskerInputInfos.fromInput(config.context, input) + private fun getTaskerIntentFromInput(stringBlurb: String?, output: TaskerOutputsForConfig, input: TaskerInput): Intent { + return Intent().apply { + val extraBundle = this.taskerPluginExtraBundle + + extraBundle.wasConfiguredBefore = true + extraBundle.runnerClass = runnerClass.name + extraBundle.inputClass = inputClass.name + getInputInfos(input).let { + val added = it.toExistingBundle(extraBundle) + val forReplacements = added.filter { it.value is String } + extraBundle.putString(TaskerPluginConstants.VARIABLE_REPLACE_KEYS, forReplacements.joinToString(" ") { it.key }) + } + + stringBlurb?.let { putExtra(TaskerPluginConstants.EXTRA_STRING_BLURB, it) } + output.add(TaskerOutputForConfig("err",context.getString(R.string.error_code) ,context.getString(R.string.error_code_description))) + output.add(TaskerOutputForConfig("errmsg",context.getString(R.string.error_message) ,context.getString(R.string.error_message_description))) + TaskerPlugin.addRelevantVariableList(this, output.map { it.toString() }.toTypedArray()) + TaskerPlugin.Setting.requestTimeoutMS(this, timeoutSeconds * 1000) + } + } + + fun finishForTasker(): SimpleResult { + val input = config.inputForTasker.apply { addInputs(this.dynamic) } + val isInputValid = isInputValid(input) + if (!isInputValid.success) return isInputValid + + val output = TaskerOutputsForConfig().apply { addOutputs(input, this) } + runner.getRenames(config.context, input)?.rename(output) + val stringBlurb = getStringBlurb(input) + + config.setResult(Activity.RESULT_OK, getTaskerIntentFromInput(stringBlurb, output, input)) + config.finish() + return isInputValid + } + + fun onBackPressed(): SimpleResult { + return finishForTasker() + } + + fun onCreate() { + config.assignFromInput(taskerIntent.getTaskerInput(config.context, inputClass, defaultInput)) + } + + private fun getStringBlurb(input: TaskerInput) = StringBuilder().apply { + if (addDefaultStringBlurb) { + getInputInfos(input).forEach { + if (it.ignoreInStringBlurb) return@forEach + val value = inputTranslationsForStringBlurb[it.key]?.invoke(it.value) ?: it.value + addTaskerInput(it.label, value) + } + } + addToStringBlurb(input, this) + }.toString() + + open val inputTranslationsForStringBlurb: HashMap String?> = HashMap() + open val addDefaultStringBlurb = true + open fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) {} + + open fun addInputs(input: TaskerInputInfos) { + + } + + open fun addOutputs(input: TaskerInput, output: TaskerOutputsForConfig) { + outputClass?.let { output.add(config.context, it, filter = { runner.shouldAddOutput(config.context, input, it) }) } + } + +} diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelperVariants.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelperVariants.kt new file mode 100644 index 00000000..f30b6e0a --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/config/TaskerPluginConfigHelperVariants.kt @@ -0,0 +1,46 @@ +package com.joaomgcd.taskerpluginlibrary.config + +import com.joaomgcd.taskerpluginlibrary.condition.* +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner + + +abstract class TaskerPluginConfigHelperNoOutput>(config: TaskerPluginConfig) : TaskerPluginConfigHelper(config) { + override val outputClass = Unit::class.java +} + +abstract class TaskerPluginConfigHelperNoInput>(config: TaskerPluginConfig) : TaskerPluginConfigHelper(config) { + override val inputClass = Unit::class.java +} + +abstract class TaskerPluginConfigHelperNoOutputOrInput>(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutput(config) { + override val inputClass = Unit::class.java + override val outputClass = Unit::class.java +} + +abstract class TaskerPluginConfigHelperConditionNoOutput>(config: TaskerPluginConfig) : TaskerPluginConfigHelper(config) { + override val outputClass = Unit::class.java +} + +abstract class TaskerPluginConfigHelperConditionNoInput>(config: TaskerPluginConfig) : TaskerPluginConfigHelper(config) { + override val inputClass = Unit::class.java +} + +abstract class TaskerPluginConfigHelperConditionNoOutputOrInput>(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutputOrInput(config) { + override val inputClass = Unit::class.java + override val outputClass = Unit::class.java +} + +abstract class TaskerPluginConfigHelperConditionNoOutputOrInputOrUpdate(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutputOrInput(config) { + override val inputClass = Unit::class.java + override val outputClass = Unit::class.java +} + +open class TaskerPluginConfigHelperEventNoOutputOrInputOrUpdate(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutputOrInput(config) { + override val runnerClass get() = TaskerPluginRunnerConditionNoOutputOrInputOrUpdateEvent::class.java + override val inputClass = Unit::class.java + override val outputClass = Unit::class.java +} +abstract class TaskerPluginConfigHelperStateNoOutputOrInputOrUpdate(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutputOrInput(config) { + override val inputClass = Unit::class.java + override val outputClass = Unit::class.java +} diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Internal.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Internal.kt new file mode 100644 index 00000000..68601925 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Internal.kt @@ -0,0 +1,107 @@ +package com.joaomgcd.taskerpluginlibrary.extensions + +import android.annotation.TargetApi +import android.app.IntentService +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.os.Build +import android.os.Bundle +import com.joaomgcd.taskerpluginlibrary.TaskerPluginConstants +import com.joaomgcd.taskerpluginlibrary.getForTaskerCompatibleInputTypes +import com.joaomgcd.taskerpluginlibrary.input.getInputFromTaskerIntent +import com.joaomgcd.taskerpluginlibrary.runner.IntentServiceParallel +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginRunner +import java.util.* + + +internal fun ArrayList?.addOrCreate(item: T) = (this ?: ArrayList()).apply { add(item) } + +internal val Intent.taskerPluginExtraBundle: Bundle + get() { + var bundle = getBundleExtra(TaskerPluginConstants.EXTRA_BUNDLE) + if (bundle == null) { + bundle = Bundle() + putExtra(TaskerPluginConstants.EXTRA_BUNDLE, bundle) + } + return bundle + } +internal var Bundle.wasConfiguredBefore: Boolean + get() = getBoolean(TaskerPluginConstants.EXTRA_WAS_CONFIGURED_BEFORE, false) + set(value) = putBoolean(TaskerPluginConstants.EXTRA_WAS_CONFIGURED_BEFORE, value) +internal var Bundle.canBindFireService: Boolean + get() = getBoolean(TaskerPluginConstants.EXTRA_CAN_BIND_FIRE_SETTING, false) + set(value) = putBoolean(TaskerPluginConstants.EXTRA_CAN_BIND_FIRE_SETTING, value) +internal val Bundle.mayNeedToStartForeground get() = !canBindFireService +internal val Intent.mayNeedToStartForeground get() = extras?.mayNeedToStartForeground ?: false + + +internal var Bundle.runnerClass: String? + get() = getString(TaskerPluginConstants.EXTRA_ACTION_RUNNER_CLASS, null) + set(value) = putString(TaskerPluginConstants.EXTRA_ACTION_RUNNER_CLASS, value) +internal var Bundle.inputClass: String? + get() = getString(TaskerPluginConstants.EXTRA_ACTION_INPUT_CLASS, null) + set(value) = putString(TaskerPluginConstants.EXTRA_ACTION_INPUT_CLASS, value) + +internal fun Intent?.getTaskerInput(context: Context, inputClass: Class, defaultInput: TInput? = null) = getInputFromTaskerIntent(context, this, inputClass, defaultInput) + +internal val Throwable.taskerErrorVariables: Bundle + get() { + val result = Bundle() + result.putString("%err", hashCode().toString()) + result.putString("%errmsg", message) + return result + } + + +internal inline fun runFromTasker(context: Context?, intent: Intent?) { + if (intent == null || context == null) return + intent.component = ComponentName(context, TService::class.java) + context.startServiceDependingOnTargetApi(intent) +} + +@TargetApi(Build.VERSION_CODES.O) +internal fun Context.startServiceDependingOnTargetApi(intent: Intent): ComponentName? = if (hasToRunServicesInForeground) { + startForegroundService(intent) +} else { + startService(intent) +} + +internal fun hasToRunServicesInForeground(targetSdkVersion: Int) = targetSdkVersion >= 26 && Build.VERSION.SDK_INT >= 26 +internal val Context.currentTargetApi + get() = applicationInfo?.targetSdkVersion ?: Build.VERSION.SDK_INT +internal val Context.hasToRunServicesInForeground get() = hasToRunServicesInForeground(currentTargetApi) +internal val ApplicationInfo.hasToRunServicesInForeground get() = hasToRunServicesInForeground(targetSdkVersion) +internal val Context.hasToRunForegroundServicesWithType get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && currentTargetApi >= 34 + +@TargetApi(Build.VERSION_CODES.O) +internal fun Context.startServiceDependingOnTargetApi(applicationInfo: ApplicationInfo, intent: Intent) = if (hasToRunServicesInForeground(applicationInfo.targetSdkVersion)) startForegroundService(intent) else startService(intent) + +internal fun Bundle.putTaskerCompatibleInput(key: String, value: Any?): Boolean { + return getForTaskerCompatibleInputTypes(value, + { false }, + { putString(key, it); true }, + { putInt(key, it); true }, + { putLong(key, it); true }, + { putFloat(key, it); true }, + { putDouble(key, it); true }, + { putBoolean(key, it); true }, + { putStringArray(key, it); true }, + { putStringArrayList(key, it); true } + ) +} + + +internal fun IntentService.startForegroundIfNeeded() = TaskerPluginRunner.startForegroundIfNeeded(this) +val String.withUppercaseFirstLetter + get() :String { + if (this.isEmpty()) return this + + return substring(0, 1).uppercase() + substring(1) + } + +val String.splitWordsTitleCase + get() = split("_").joinToString(" ") { + it.withUppercaseFirstLetter + } \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Public.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Public.kt new file mode 100644 index 00000000..1cf29a68 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/extensions/Public.kt @@ -0,0 +1,50 @@ +package com.joaomgcd.taskerpluginlibrary.extensions + +import android.app.Activity +import android.content.Context +import com.joaomgcd.taskerpluginlibrary.condition.TaskerPluginRunnerCondition + +/** + * Will convert a string to a Tasker compatible variable name + */ +val String.taskerOutputCompatible: String + get() { + var s = this.trim().replace(" ", "").replace("%", "").replace(" ", "_").replace("[]", "").replace("-", "").replace("'", "_").replace("\\[[0-9]+\\]".toRegex(), "").toLowerCase() + if (s.length < 3) { + s = pad(s, "a", 3, false) + } + return s + } + +/** + * Adds a label and string followed by a new line in the following format - Label: value + * Can be handy to use when building the string blub output for the Tasker condition/action + */ +fun StringBuilder.addTaskerInput(label: String?, value: Any?) { + if (value == null || label == null) return + + val finalValue = when (value) { + is Boolean -> if (value) "true" else null + else -> value.toString() + } + if (finalValue.isNullOrEmpty()) return + + if (length > 0) { + append("\n") + } + append("$label: $finalValue") +} + +/** + * Shorthand method to request Tasker to query a certain condition + */ +fun Class.requestQuery(context: Context, update: Any? = null) = TaskerPluginRunnerCondition.requestQuery(context, this, update) + + +private fun pad(s: String, ch: String, n: Int, right: Boolean): String { + val realN = n - s.length + val pad = String.format("%0" + realN + "d", 0).replace("0", ch) + val leftPad = if (right) "" else pad + val rightPad = if (right) pad else "" + return leftPad + s + rightPad +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerInputField.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerInputField.kt new file mode 100644 index 00000000..c88b55c9 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerInputField.kt @@ -0,0 +1,16 @@ +package com.joaomgcd.taskerpluginlibrary.input + +import com.joaomgcd.taskerpluginlibrary.STRING_RES_ID_NAME_NOT_SET +import com.joaomgcd.taskerpluginlibrary.STRING_RES_ID_NOT_SET + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class TaskerInputRoot() + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class TaskerInputField(val key: String, val labelResId: Int = STRING_RES_ID_NOT_SET, val descriptionResId: Int = STRING_RES_ID_NOT_SET, val ignoreInStringBlurb: Boolean = false, val order: Int = Int.MAX_VALUE, val labelResIdName:String= STRING_RES_ID_NAME_NOT_SET, val descriptionResIdName:String= STRING_RES_ID_NAME_NOT_SET) + +@Target(AnnotationTarget.CLASS,AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class TaskerInputObject(val key: String, val labelResId: Int = STRING_RES_ID_NOT_SET, val descriptionResId: Int = STRING_RES_ID_NOT_SET, val ignoreInStringBlurb: Boolean = false, val order: Int = Int.MAX_VALUE, val labelResIdName:String= STRING_RES_ID_NAME_NOT_SET, val descriptionResIdName:String= STRING_RES_ID_NAME_NOT_SET) \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerPluginInput.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerPluginInput.kt new file mode 100644 index 00000000..dbc0414c --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/input/TaskerPluginInput.kt @@ -0,0 +1,160 @@ +package com.joaomgcd.taskerpluginlibrary.input + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.joaomgcd.taskerpluginlibrary.NoEmptyConstructorException +import com.joaomgcd.taskerpluginlibrary.STRING_RES_ID_NOT_SET +import com.joaomgcd.taskerpluginlibrary.extensions.putTaskerCompatibleInput +import com.joaomgcd.taskerpluginlibrary.extensions.taskerPluginExtraBundle +import com.joaomgcd.taskerpluginlibrary.extensions.wasConfiguredBefore +import com.joaomgcd.taskerpluginlibrary.getForTaskerCompatibleInputTypes +import com.joaomgcd.taskerpluginlibrary.getStringFromResourceIdOrResourceName +import java.lang.reflect.Field + + +open class TaskerInputInfo(val key: String, val label: String?, val description: String?, val ignoreInStringBlurb: Boolean, open var value: Any?, val order: Int = Int.MAX_VALUE) { + fun valueAs() = value as T +} + +open class TaskerInputInfoDynamic(key: String, label: String?, description: String?, ignoreInStringBlurb: Boolean, private val getter: () -> Any?, private val setter: ((Any?) -> Unit)? = null, order: Int = Int.MAX_VALUE) : TaskerInputInfo(key, label, description, ignoreInStringBlurb, null, order) { + val Any?.isEmpty + get() = getForTaskerCompatibleInputTypes(this, + { true }, + { it.isEmpty() }, + { false }, + { false }, + { false }, + { false }, + { false }, + { false }, + { false }) + override var value: Any? + get() { + return try { + getter() + } catch (t: Throwable) { + t.printStackTrace() + null + } + } + set(value) { + if (value == null || value.isEmpty) return + try { + setter?.invoke(value) + } catch (t: Throwable) { + t.printStackTrace() + } + } + + +} + +class TaskerInputInfoField(key: String, label: String, description: String?, ignoreInStringBlurb: Boolean, val taskerPluginInput: Any, private val getter: Field, order: Int = Int.MAX_VALUE) : + TaskerInputInfoDynamic(key, label, description, ignoreInStringBlurb, { getter.apply { isAccessible = true }.get(taskerPluginInput) }, { getter.apply { isAccessible = true }.set(taskerPluginInput, it) }, order) { +// companion object { +// private fun getSetter(taskerPluginInput: Any, getter: Method): ((Any?) -> Unit)? { +// return { +// +// val setter: Method? = try { +// taskerPluginInput::class.java.getDeclaredMethod(getter.name.replace("get", "set"), getter.returnType) +// } catch (t: Throwable) { +// t.printStackTrace() +// null +// } +// setter?.invoke(taskerPluginInput, it) +// } +// } +// } + +} + +class TaskerInputInfos : ArrayList() { + companion object { + fun fromInput(context: Context, input: TaskerInput<*>) = fromInput(context, input.regular).apply { addAll(input.dynamic) } + fun fromInput(context: Context, input: Any) = TaskerInputInfos().apply { addFromInput(context, input) } + fun fromBundle(context: Context, input: Any, bundle: Bundle) = fromInput(context, input).apply { + forEach { + it.value = bundle.get(it.key) + } + bundle.keySet().forEach { key -> + bundle.get(key)?.let { value -> add(TaskerInputInfo(key, null, null, true, value)) } + + } + } + } + + private fun getString(context: Context, resId: Int, resIdName: String, defaultString:String) = context.getStringFromResourceIdOrResourceName(resId,resIdName,defaultString) + + fun addFromInput(context: Context, taskerPluginInput: Any, parentKey: String? = null) { + val inputClass = taskerPluginInput::class.java + if (inputClass == Unit::class.java) return + if (!inputClass.isAnnotationPresent(TaskerInputRoot::class.java) && !inputClass.isAnnotationPresent(TaskerInputObject::class.java)) { + throw RuntimeException("Input types must be annotated by either TaskerInputRoot or TaskerInputObject. $inputClass has none.") + } + val fields = inputClass.declaredFields + val (inputFields, other) = fields.partition { it.isAnnotationPresent(TaskerInputField::class.java) } + addAll(inputFields + .map { method -> + val annotation = method.getAnnotation(TaskerInputField::class.java) + var key = annotation.key + if (parentKey != null) key = "$parentKey.$key" + val label = getString(context, annotation.labelResId,annotation.labelResIdName,key) + val description = getString(context, annotation.descriptionResId, annotation.descriptionResIdName,"") + TaskerInputInfoField(key, label, description, annotation.ignoreInStringBlurb, taskerPluginInput, method) + } + ) + other + .filter { it.type.isAnnotationPresent(TaskerInputObject::class.java) } + .forEach { method -> + val annotation = method.type.getAnnotation(TaskerInputObject::class.java) + val inputObject: Any? = method.apply { isAccessible = true }.get(taskerPluginInput) + inputObject?.let { + var key = annotation.key + method.getAnnotation(TaskerInputObject::class.java)?.key?.let { key = "$key.$it" } + addFromInput(context, it, key) + } + } + + + } + + fun getByKey(key: String) = firstOrNull { it.key == key } + val bundle get() = Bundle().apply { toExistingBundle(this) } + fun toExistingBundle(bundle: Bundle) = filter { bundle.putTaskerCompatibleInput(it.key, it.value) } +} + +class TaskerInput(val regular: TInput, val dynamic: TaskerInputInfos = TaskerInputInfos()) + +internal fun getInputFromTaskerIntent(context: Context, taskerIntent: Intent?, inputClass: Class, defaultInput: TInput? = null): TaskerInput { + // if (inputClass == Unit::class.java) return TaskerInput(Unit) as TaskerInput + fun getNewInstance() = try { + if (inputClass == Unit::class.java) { + Unit + } else { + inputClass.newInstance() + } as TInput + + } catch (t: Throwable) { + throw NoEmptyConstructorException(inputClass.name) + } + + fun getInput(): TInput { + val wasConfiguredBefore = taskerIntent?.taskerPluginExtraBundle?.wasConfiguredBefore + ?: false + return if (!wasConfiguredBefore) { + defaultInput ?: getNewInstance() + } else { + getNewInstance() + } + } + + + + if (taskerIntent == null) return TaskerInput(getInput()) + val extrasBundle = taskerIntent.taskerPluginExtraBundle + val input = getInput() + val inputInfos = TaskerInputInfos.fromBundle(context, input, extrasBundle) + return TaskerInput(input, inputInfos) +} + diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerOutputVariable.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerOutputVariable.kt new file mode 100644 index 00000000..ccb44acf --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerOutputVariable.kt @@ -0,0 +1,13 @@ +package com.joaomgcd.taskerpluginlibrary.output + +import com.joaomgcd.taskerpluginlibrary.STRING_RES_ID_NAME_NOT_SET +import com.joaomgcd.taskerpluginlibrary.STRING_RES_ID_NOT_SET + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) +@Retention(AnnotationRetention.RUNTIME) +annotation class TaskerOutputVariable(val name: String, val labelResId: Int = STRING_RES_ID_NOT_SET, val htmlLabelResId: Int = STRING_RES_ID_NOT_SET, val minApi: Int = -1, val maxApi: Int = Int.MAX_VALUE, val labelResIdName:String=STRING_RES_ID_NAME_NOT_SET, val htmlLabelResIdName:String=STRING_RES_ID_NAME_NOT_SET) + + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class TaskerOutputObject() \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputBase.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputBase.kt new file mode 100644 index 00000000..3e205e1c --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputBase.kt @@ -0,0 +1,99 @@ +package com.joaomgcd.taskerpluginlibrary.output + +import android.content.Context +import android.os.Build +import com.joaomgcd.taskerpluginlibrary.extensions.addOrCreate +import com.joaomgcd.taskerpluginlibrary.extensions.taskerOutputCompatible +import java.lang.reflect.Method + + + + +abstract class TaskerOuputBase(var nameNoSuffix: String, val isMultiple: Boolean = false, val minApi: Int = -1, val maxApi: Int = Int.MAX_VALUE, var ignore: Boolean = false) { + + val nameTaskerCompatible get() = nameNoSuffix.taskerOutputCompatible + val name: String + get() { + val result = nameTaskerCompatible + return if (!isMultiple) result else "$result()" + } + + constructor(context: Context, taskerVariable: TaskerOutputVariable, isMultiple: Boolean = false) : this(taskerVariable.name, isMultiple, taskerVariable.minApi, taskerVariable.maxApi) + constructor(context: Context, taskerVariable: TaskerOutputVariable, method: Method) : this(context, taskerVariable, method.returnType.isArray) + +} + + +abstract class TaskerOutputBase : ArrayList() { + abstract fun getTaskerVariable(context: Context, taskerVariable: TaskerOutputVariable, method: Method, parent: Any?, isThisList: Boolean, isBaseList: Boolean, index: ArrayList? = null): List + fun add(context: Context, type: Class<*>, parent: Any? = null, filter: ((TTaskerVariable) -> Boolean)? = null, isBaseList: Boolean = false, index: ArrayList? = null) { + var realType = type + val isList = realType.isArray + if (isList) { + realType = realType.componentType + } + val methods = realType.methods + val (variables, other) = methods.partition { it.isAnnotationPresent(TaskerOutputVariable::class.java) } + addAll(variables + .map { + getTaskerVariable(context, it.getAnnotation(TaskerOutputVariable::class.java), it, parent, isList, isBaseList, index) + }.flatten().map { + it.apply { ignore = filter?.let { filter -> !filter(this) } ?: false } + } + .filtered + ) + other + .filter { it.returnType.isAnnotationPresent(TaskerOutputObject::class.java) || it.returnType.componentType?.isAnnotationPresent(TaskerOutputObject::class.java) == true } + .forEach { method -> + if (parent == null) { + add(context, method.returnType, parent, filter, isList) + } else { + if (!isList) { + add(context, method.returnType, method.invoke(parent), filter, isList) + } else { + (parent as Array<*>).forEachIndexed { indexForThis, parent -> + add(context, method.returnType, method.invoke(parent), filter, isList, index.addOrCreate(indexForThis + 1)) + } + + } + } + } + } + + + // inline fun add(context: Context) = add(context, T::class.java) + fun add(vararg taskerVariableInfo: TTaskerVariable) { + addAll(taskerVariableInfo.filtered) + } + + override fun add(element: TTaskerVariable): Boolean { + if (!getTaskerFilter(element)) return false + return super.add(element) + } + + override fun add(index: Int, element: TTaskerVariable) { + if (!getTaskerFilter(element)) return + super.add(index, element) + } + + override fun addAll(elements: Collection): Boolean { + return super.addAll(elements.filtered) + } + + override fun addAll(index: Int, elements: Collection): Boolean { + return super.addAll(index, elements.filtered) + } + + + fun getByName(name: String) = firstOrNull { it.nameNoSuffix == name } + + fun renameIfNeeded(oldName: String, newName: CharSequence?) { + if (newName == null || newName.isEmpty()) return + getByName(oldName)?.nameNoSuffix = newName.toString() + } + + private val Collection.filtered get() = filterForTasker(this) + private val Array.filtered get() = filterForTasker(this.toList()) + private fun filterForTasker(output: Collection): List = output.filter { getTaskerFilter(it) } + private fun getTaskerFilter(it: TTaskerVariable) = !it.ignore && Build.VERSION.SDK_INT >= it.minApi && Build.VERSION.SDK_INT <= it.maxApi +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputForConfig.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputForConfig.kt new file mode 100644 index 00000000..ba3c7a59 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/TaskerPluginOutputForConfig.kt @@ -0,0 +1,27 @@ +package com.joaomgcd.taskerpluginlibrary.output + +import android.content.Context +import com.joaomgcd.taskerpluginlibrary.extensions.splitWordsTitleCase +import com.joaomgcd.taskerpluginlibrary.getStringFromResourceIdOrResourceName +import java.lang.reflect.Method + + +class TaskerOutputForConfig(nameNoSuffix: String, val label: String, val htmlLabel: String, isMultiple: Boolean = false, minApi: Int = -1, maxApi: Int = Int.MAX_VALUE) : TaskerOuputBase(nameNoSuffix, isMultiple, minApi, maxApi) { + constructor(context: Context, taskerVariable: TaskerOutputVariable, isMultiple: Boolean = false) : this( + taskerVariable.name, + context.getStringFromResourceIdOrResourceName(taskerVariable.labelResId, taskerVariable.labelResIdName, taskerVariable.name.splitWordsTitleCase), + context.getStringFromResourceIdOrResourceName(taskerVariable.htmlLabelResId, taskerVariable.htmlLabelResIdName, ""), + isMultiple, + taskerVariable.minApi, + taskerVariable.maxApi + ) + + constructor(context: Context, taskerVariable: TaskerOutputVariable, method: Method, isThisList: Boolean, isBaseList: Boolean) : this(context, taskerVariable, method.returnType.isArray || isThisList || isBaseList) + + override fun toString() = "%$name\n$label\n$htmlLabel" +} + +class TaskerOutputsForConfig : TaskerOutputBase() { + override fun getTaskerVariable(context: Context, taskerVariable: TaskerOutputVariable, method: Method, parent: Any?, isThisList: Boolean, isBaseList: Boolean, index: ArrayList?) = TaskerOutputsForConfig().apply { add(TaskerOutputForConfig(context, taskerVariable, method, isThisList, isBaseList)) } + +} diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputForRunner.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputForRunner.kt new file mode 100644 index 00000000..09dee5f8 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputForRunner.kt @@ -0,0 +1,105 @@ +package com.joaomgcd.taskerpluginlibrary.output.runner + +import android.content.Context +import android.os.Bundle +import com.joaomgcd.taskerpluginlibrary.extensions.addOrCreate +import com.joaomgcd.taskerpluginlibrary.output.TaskerOuputBase +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputBase +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable +import com.joaomgcd.taskerpluginlibrary.runner.TaskerOutputRenames +import java.lang.reflect.Method + + + + +open class TaskerOutputForRunner(nameNoSuffix: String, val valueGetter: TaskerValueGetter, val parent: Any? = null, minApi: Int = -1, maxApi: Int = Int.MAX_VALUE, val index: ArrayList? = null) : TaskerOuputBase(getName(nameNoSuffix, index), valueGetter.isArray, minApi, maxApi) { + constructor(nameNoSuffix: String, value: String?) : this(nameNoSuffix, TaskerValueGetterDirect(value)) + constructor(nameNoSuffix: String, value: Array<*>?) : this(nameNoSuffix, TaskerValueGetterDirect(value)) + constructor(nameNoSuffix: String, value: Collection<*>?) : this(nameNoSuffix, TaskerValueGetterDirect(value?.toTypedArray())) + + constructor(context: Context, taskerVariable: TaskerOutputVariable, valueGetter: TaskerValueGetter, parent: Any?, index: ArrayList? = null) : this(taskerVariable.name, valueGetter, parent, taskerVariable.minApi, taskerVariable.maxApi, index) + + val value get() = valueGetter.getValue(parent) + fun addToBundle(bundle: Bundle) { + val value = this.value ?: return + val values = getValues(value) + if (values.isEmpty()) return + + val name = nameTaskerCompatible + for (i in 0 until values.size) { + val indexedName = if (isMultiple) { + "$name${i + 1}" + } else { + name + } + bundle.putString("%$indexedName", values[i]) + } + } + + private fun getValues(value: Any): Array { + val result: Array<*> = if (isMultiple) { + value as Array<*> + } else { + arrayOf(value) + } + return result.map { it.toString() }.toTypedArray() + } + + private companion object { + fun getName(name: String, index: ArrayList?): String { + if (index == null || index.size == 0) return name + return "$name${index[0]}" + } + } +} + + +class TaskerOutputsForRunner : TaskerOutputBase() { + override fun getTaskerVariable(context: Context, taskerVariable: TaskerOutputVariable, method: Method, parent: Any?, isThisList: Boolean, isBaseList: Boolean, index: ArrayList?): TaskerOutputsForRunner { + val result = TaskerOutputsForRunner() + if (!isThisList) { + return result.apply { add(TaskerOutputForRunner(context, taskerVariable, TaskerValueGetterMethod(method), parent, index)) } + } else { + val parentAsArray = parent as Array<*> + for (i in 0 until parentAsArray.size) { + val parentInArray = parentAsArray[i] + result.add(TaskerOutputForRunner(context, taskerVariable, TaskerValueGetterMethod(method), parentInArray, index.addOrCreate(i + 1))) + } + return result + } + } + + + companion object { + private data class NameAndIndex(val name: String, val index: Int?) + + + + @JvmStatic + fun getVariableBundle(context: Context, regularOutput: Any? = null, dynamicOutput: TaskerOutputsForRunner? = null, renames: TaskerOutputRenames? = null, filter: ((TaskerOutputForRunner) -> Boolean)? = null): Bundle { + return Bundle().apply { + TaskerOutputsForRunner().apply { + dynamicOutput?.let { addAll(it) } + }.apply { + regularOutput?.let { add(context, it::class.java, it, filter) } + }.apply { + renames?.rename(this) + }.groupBy { + val index = if (it.index == null || it.index.size == 0) null else it.index[0] + NameAndIndex(it.name, index) + }.map { group -> + val first = group.value[0] + if (group.value.size == 1) { + first + } else { + val value = group.value.joinToString(",") { output -> first.valueGetter.getValue(output.parent).toString() } + TaskerOutputForRunner(first.name, value) + + } + }.forEach { + it.addToBundle(this) + } + } + } + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputValueGetter.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputValueGetter.kt new file mode 100644 index 00000000..347f1166 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/output/runner/TaskerPluginOutputValueGetter.kt @@ -0,0 +1,40 @@ +package com.joaomgcd.taskerpluginlibrary.output.runner + +import java.lang.reflect.Method + + + +sealed class TaskerValueGetter { + abstract fun getValue(obj: Any?): Any? + abstract val isArray: Boolean +} + +class TaskerValueGetterMethod(val method: Method) : TaskerValueGetter() { + override fun getValue(obj: Any?): Any? { + if (obj == null) return null + return try { + val value = method.invoke(obj) ?: return null + when (value) { + is String -> value + is Array<*> -> value + is Boolean, Int, Long -> value.toString() + is Float -> value.toString() + is Double -> value.toString() + is Collection<*> -> value.toTypedArray() + else -> value.toString() + } + } catch (t: Throwable) { + null + } + } + + override val isArray = method.returnType.isArray +} + +class TaskerValueGetterDirect private constructor(val value: Any?) : TaskerValueGetter() { + constructor(value: String?) : this(value as Any?) + constructor(value: Array<*>?) : this(value as Any?) + + override fun getValue(obj: Any?): Any? = value + override val isArray = if (value == null) false else value::class.java.isArray +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/IntentServiceParallel.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/IntentServiceParallel.kt new file mode 100644 index 00000000..f8ff41f3 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/IntentServiceParallel.kt @@ -0,0 +1,96 @@ +package com.joaomgcd.taskerpluginlibrary.runner + +import android.app.IntentService +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.Handler +import android.os.Looper +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger + +/** + * A drop-in replacement for IntentService that performs various tasks at the same time instead of one after the other + * + * @property name Name for the service. Will be used to name the threads created in this service. + */ +abstract class IntentServiceParallel(val name: String) : Service() { + /** + * Simply call [onStart] like [IntentService] does + */ + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + onStart(intent, startId) + return Service.START_NOT_STICKY + } + + private val binder by lazy { Binder() } + fun startForegroundIfNeeded(mayNeedToStartForeground: Boolean = true) = TaskerPluginRunner.startForegroundIfNeeded(this, mayNeedToStartForeground = mayNeedToStartForeground) + override fun onBind(intent: Intent) = binder + protected abstract fun onHandleIntent(intent: Intent) + + /** + * Use a handler on the main thread to post exceptions and stop the service when all tasks are done + */ + private val handler = Handler(Looper.getMainLooper()) + + /** + * Keep count of how many tasks are active so that we can stop when they reach 0 + */ + private var jobsCount: AtomicInteger = AtomicInteger(0) + + /** + * + * The executor to run tasks in parallel threads + * + */ + private val executor by lazy { Executors.newCachedThreadPool { runnable -> Thread(runnable, "IntentServiceParallel$name") } } + + /** + * Keep track of the last startId sent to the service. Will be used to make sure we only stop the service if the last startId was actually the last startId that the service received. + */ + private var lastStartId: Int? = null + + /** + * Main function of the class. Starts processing each new task in parallel with existing tasks. When all tasks are processed will stop itself. Will ignore null intents. + */ + override fun onStart(intent: Intent?, startId: Int) { + if (intent == null) return + + //Count +1 so that we know how many tasks are running + jobsCount.addAndGet(1) + + /** + * store the startId so that we will always use [stopSelf] with the correct Id. + * This is stored after incrementing the count so that if [stopSelf] runs before + * the increment the service is not stopped because the last startId is not used as a parameter + */ + lastStartId = startId + executor.submit { + try { + //run task in parallel + onHandleIntent(intent) + } catch (throwable: RuntimeException) { + //throw any exception in the main thread + handler.post { throw throwable } + } finally { + handler.post { + //decrement in the main thread to avoid concurrency issues + if (jobsCount.decrementAndGet() > 0) return@post + + //stop only if lastStartId was the last startId that was posted + lastStartId.let { if (it != null) stopSelf(it) else stopSelf() } + } + } + } + + } + + + /** + * Shutdown the executor when the sevice is destroyed + */ + override fun onDestroy() { + super.onDestroy() + executor.shutdown() + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerOutputRenames.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerOutputRenames.kt new file mode 100644 index 00000000..a48a9540 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerOutputRenames.kt @@ -0,0 +1,13 @@ +package com.joaomgcd.taskerpluginlibrary.runner + +import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputBase + +class TaskerOutputRename(val oldValue: String, val newValue: CharSequence?) +class TaskerOutputRenames : ArrayList() { + fun rename(infos: TaskerOutputBase<*>) { + forEach { infos.renameIfNeeded(it.oldValue, it.newValue) } + } + + companion object { + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResult.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResult.kt new file mode 100644 index 00000000..58606ee7 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResult.kt @@ -0,0 +1,37 @@ +package com.joaomgcd.taskerpluginlibrary.runner + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import com.joaomgcd.taskerpluginlibrary.output.runner.TaskerOutputForRunner +import com.joaomgcd.taskerpluginlibrary.output.runner.TaskerOutputsForRunner +import net.dinglisch.android.tasker.TaskerPlugin + +class ArgsSignalFinish(val context: Context, val taskerIntent: Intent, val renames: TaskerOutputRenames? = null, val filter: ((TaskerOutputForRunner) -> Boolean)? = null) +sealed class TaskerPluginResult(val success: Boolean) { + abstract fun signalFinish(args: ArgsSignalFinish): Boolean +} + +class TaskerPluginResultSucess(val regular: TOutput? = null, val dynamic: TaskerOutputsForRunner? = null, val callbackUri: Uri? = null) : TaskerPluginResult(true) { + override fun signalFinish(args: ArgsSignalFinish) = TaskerPlugin.Setting.signalFinish(args.context, args.taskerIntent, TaskerPlugin.Setting.RESULT_CODE_OK, TaskerOutputsForRunner.getVariableBundle(args.context, regular, dynamic, args.renames, args.filter), callbackUri) +} + +class TaskerPluginResultError(code: Int, message: String) : TaskerPluginResultErrorWithOutput(code, message) { + constructor(t: Throwable) : this(t.hashCode(), t.message ?: t.toString()) +} + +open class TaskerPluginResultErrorWithOutput(private val code: Int, val message: String) : TaskerPluginResult(false) { + override fun signalFinish(args: ArgsSignalFinish) = + TaskerPlugin.Setting.signalFinish(args.context, args.taskerIntent, TaskerPlugin.Setting.RESULT_CODE_FAILED, Bundle().apply { + putString("%err", code.toString()) + putString("%errmsg", message) + }) + + constructor(t: Throwable) : this(t.hashCode(), t.message ?: t.toString()) +} + + +class TaskerPluginResultUnknown() : TaskerPluginResult(false) { + override fun signalFinish(args: ArgsSignalFinish) = false +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResultCondition.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResultCondition.kt new file mode 100644 index 00000000..8d06fcdd --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginResultCondition.kt @@ -0,0 +1,25 @@ +package com.joaomgcd.taskerpluginlibrary.runner + +import android.content.Context +import com.joaomgcd.taskerpluginlibrary.TaskerPluginConstants +import com.joaomgcd.taskerpluginlibrary.output.runner.TaskerOutputForRunner +import com.joaomgcd.taskerpluginlibrary.output.runner.TaskerOutputsForRunner + +sealed class TaskerPluginResultCondition(val success: Boolean) { + abstract val conditionResultCode: Int +} + +class TaskerPluginResultConditionSatisfied(val context: Context, val regular: TOutput? = null, val dynamic: TaskerOutputsForRunner? = null) : TaskerPluginResultCondition(true) { + override val conditionResultCode = TaskerPluginConstants.RESULT_CONDITION_SATISFIED + fun getOutputBundle(renames: TaskerOutputRenames? = null, filter: ((TaskerOutputForRunner) -> Boolean)) = TaskerOutputsForRunner.getVariableBundle(context, regular, dynamic, renames, filter) +} + +class TaskerPluginResultConditionUnsatisfied() : TaskerPluginResultCondition(false) { + override val conditionResultCode = TaskerPluginConstants.RESULT_CONDITION_UNSATISFIED +} + + +class TaskerPluginResultConditionUnknown() : TaskerPluginResultCondition(false) { + + override val conditionResultCode = TaskerPluginConstants.RESULT_CONDITION_UNKNOWN +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginRunner.kt b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginRunner.kt new file mode 100644 index 00000000..f217b1a8 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/com/joaomgcd/taskerpluginlibrary/runner/TaskerPluginRunner.kt @@ -0,0 +1,125 @@ +package com.joaomgcd.taskerpluginlibrary.runner + +import android.annotation.TargetApi +import android.app.* +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.graphics.drawable.Icon +import android.os.Build +import com.joaomgcd.taskerpluginlibrary.R +import com.joaomgcd.taskerpluginlibrary.extensions.hasToRunForegroundServicesWithType +import com.joaomgcd.taskerpluginlibrary.extensions.hasToRunServicesInForeground +import com.joaomgcd.taskerpluginlibrary.extensions.inputClass +import com.joaomgcd.taskerpluginlibrary.extensions.runnerClass +import com.joaomgcd.taskerpluginlibrary.extensions.taskerPluginExtraBundle +import com.joaomgcd.taskerpluginlibrary.input.TaskerInput +import com.joaomgcd.taskerpluginlibrary.output.TaskerOuputBase + + +abstract class TaskerPluginRunner { + /** + * Gets the input class for the runner from the extras bundle + */ + fun getInputClass(taskerIntent: Intent): Class = Class.forName(taskerIntent.taskerPluginExtraBundle.inputClass) as Class + + /** + * Notification Properties used to show the foreground notification on Android O or above + */ + class NotificationProperties @JvmOverloads constructor( + val notificationChannelNameResId: Int = R.string.tasker_plugin_service, + val notificationChannelDescriptionResId: Int = R.string.tasker_plugin_service_description, + val titleResId: Int = R.string.app_name, + val textResId: Int = R.string.running_tasker_plugin, + val iconResId: Int = R.mipmap.ic_launcher, + val notificationChannelId: String = NOTIFICATION_CHANNEL_ID, + val notificationBuilderExtender: Notification.Builder.(context: Context) -> Notification.Builder = { this } + ) { + @TargetApi(Build.VERSION_CODES.O) + fun getNotification(context: Context) = Notification.Builder(context, notificationChannelId) + .setContentTitle(context.getString(titleResId)) + .setContentText(context.getString(textResId)) + .setSmallIcon(Icon.createWithResource(context, iconResId)) + .notificationBuilderExtender(context) + .build() + } + + /** + * Can be overriden so that plugins can present customized foreground notifications when they are executing on Android O or above + */ + protected open val notificationProperties get() = NotificationProperties() + + @TargetApi(Build.VERSION_CODES.O) + protected fun IntentService.startForegroundIfNeeded() { + TaskerPluginRunner.startForegroundIfNeeded(this, notificationProperties) + } + + @TargetApi(Build.VERSION_CODES.O) + fun startForegroundIfNeeded(intentServiceParallel: IntentServiceParallel) { + TaskerPluginRunner.startForegroundIfNeeded(intentServiceParallel, notificationProperties) + } + + + companion object { + private const val NOTIFICATION_CHANNEL_ID = "taskerpluginforegroundd" + + @TargetApi(Build.VERSION_CODES.O) + fun Service.createNotificationChannel(notificationProperties: NotificationProperties) { + val notificationManager = getSystemService(NotificationManager::class.java) + val channel = NotificationChannel(notificationProperties.notificationChannelId, getString(notificationProperties.notificationChannelNameResId), NotificationManager.IMPORTANCE_NONE) + channel.description = getString(notificationProperties.notificationChannelDescriptionResId) + notificationManager.createNotificationChannel(channel) + } + + /** + * Will start an IntentService in the foreground so that the app doesn't crash if it takes more than 5 seconds to execute + * @param mayNeedToStartForeground if false, don't even check if it needs to start in the foreground, otherwise check normally + */ + @TargetApi(Build.VERSION_CODES.O) + fun startForegroundIfNeeded(intentService: Service, notificationProperties: NotificationProperties = NotificationProperties(), mayNeedToStartForeground: Boolean = true) { + if (!mayNeedToStartForeground) return + if (!intentService.hasToRunServicesInForeground) return + intentService.createNotificationChannel(notificationProperties) + val notification: Notification = notificationProperties.getNotification(intentService) + if (!intentService.hasToRunForegroundServicesWithType) return intentService.startForeground(this.hashCode(), notification) + + intentService.startForeground(this.hashCode(), notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } + + /** + * Gets the plugin runner from the extras bundle + */ + internal inline fun > getFromTaskerIntent(taskerIntent: Intent?): TRunner? { + val runnerClass = taskerIntent?.taskerPluginExtraBundle?.runnerClass + ?: return null + + val clazz = try { + Class.forName(runnerClass) + } catch (t: Throwable) { + t.printStackTrace() + null + } ?: return null + + return try { + clazz.newInstance() as TRunner + } catch (t: Throwable) { + t.printStackTrace() + null + } + } + + } + + internal fun getRenames(context: Context, input: TaskerInput?) = input?.let { TaskerOutputRenames().apply { addOutputVariableRenames(context, input, this) } } + + /** + * Allows plugin developer to rename output variables based on user input. This allows user to choose his/her own names for output variables. Check GetTimeRunner example in Tasker Plugin Sample + */ + open fun addOutputVariableRenames(context: Context, input: TaskerInput, renames: TaskerOutputRenames) {} + + /** + * Allows plugin developer to not output certain values. Useful because sometimes actions can return too many outputs, so this allows you to trim it down to just the needed + */ + open fun shouldAddOutput(context: Context, input: TaskerInput?, ouput: TaskerOuputBase) = true + +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/PluginResultReceiver.kt b/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/PluginResultReceiver.kt new file mode 100644 index 00000000..0ba22328 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/PluginResultReceiver.kt @@ -0,0 +1,7 @@ +package net.dinglisch.android.tasker + +import android.os.Handler +import android.os.ResultReceiver + + +class PluginResultReceiver(handler: Handler?) : ResultReceiver(handler) \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerIntent.java b/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerIntent.java new file mode 100644 index 00000000..1b2054b3 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerIntent.java @@ -0,0 +1,449 @@ +// Version 1.3.3 + +// Changelog + +// Version 1.3.3 +// - increased MAX_NO_ARGS to 10 + +// Version 1.3.2 +// - bug setting app arg +// - pulled provider column names out of function + +// For usage examples see http://tasker.dinglisch.net/invoketasks.html + +package net.dinglisch.android.tasker; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.PatternMatcher; +import android.os.Process; +import android.util.Log; + +public class TaskerIntent extends Intent { + + // 3 Tasker versions + public final static String TASKER_PACKAGE = "net.dinglisch.android.tasker"; + public final static String TASKER_PACKAGE_MARKET = TASKER_PACKAGE + "m"; + public final static String TASKER_PACKAGE_CUPCAKE = TASKER_PACKAGE + "cupcake"; + + // Play Store download URLs + public final static String MARKET_DOWNLOAD_URL_PREFIX = "market://details?id="; + private final static String TASKER_MARKET_URL = MARKET_DOWNLOAD_URL_PREFIX + TASKER_PACKAGE_MARKET; + private final static String TASKER_MARKET_URL_CUPCAKE = MARKET_DOWNLOAD_URL_PREFIX + TASKER_PACKAGE_CUPCAKE; + + // Direct-purchase version + private final static String TASKER_DOWNLOAD_URL = "http://tasker.dinglisch.net/download.html"; + + // Intent actions + public final static String ACTION_TASK = TASKER_PACKAGE + ".ACTION_TASK"; + public final static String ACTION_TASK_COMPLETE = TASKER_PACKAGE + ".ACTION_TASK_COMPLETE"; + public final static String ACTION_TASK_SELECT = TASKER_PACKAGE + ".ACTION_TASK_SELECT"; + + // Intent parameters + public final static String EXTRA_ACTION_INDEX_PREFIX = "action"; + public final static String TASK_NAME_DATA_SCHEME = "task"; + public final static String EXTRA_TASK_NAME = "task_name"; + public final static String EXTRA_TASK_PRIORITY = "task_priority"; + public final static String EXTRA_SUCCESS_FLAG = "success"; + public final static String EXTRA_VAR_NAMES_LIST = "varNames"; + public final static String EXTRA_VAR_VALUES_LIST = "varValues"; + public final static String EXTRA_TASK_OUTPUT = "output"; + + // Content provider columns + public static final String PROVIDER_COL_NAME_EXTERNAL_ACCESS = "ext_access"; + public static final String PROVIDER_COL_NAME_ENABLED = "enabled"; + + // DEPRECATED, use EXTRA_VAR_NAMES_LIST, EXTRA_VAR_VALUES_LIST + public final static String EXTRA_PARAM_LIST = "params"; + + // Intent data + + public final static String TASK_ID_SCHEME = "id"; + + // For particular actions + + public final static String DEFAULT_ENCRYPTION_KEY= "default"; + public final static String ENCRYPTED_AFFIX = "tec"; + public final static int MAX_NO_ARGS = 10; + + // Bundle keys + // Only useful for Tasker + public final static String ACTION_CODE = "action"; + public final static String APP_ARG_PREFIX = "app:"; + public final static String ICON_ARG_PREFIX = "icn:"; + public final static String ARG_INDEX_PREFIX = "arg:"; + public static final String PARAM_VAR_NAME_PREFIX = "par"; + + // Misc + private final static String PERMISSION_RUN_TASKS = TASKER_PACKAGE + ".PERMISSION_RUN_TASKS"; + + public final static String ACTION_OPEN_PREFS = TASKER_PACKAGE + ".ACTION_OPEN_PREFS"; + public final static String EXTRA_OPEN_PREFS_TAB_NO = "tno"; + private final static int MISC_PREFS_TAB_NO = 3; // 0 based + + // To query whether Tasker is enabled and external access is enabled + private final static String TASKER_PREFS_URI = "content://" + TASKER_PACKAGE + "/prefs"; + + private final static int CUPCAKE_SDK_VERSION = 3; + + // result values for TestSend + + // NotInstalled: Tasker package not found on device + // NoPermission: calling app does not have permission PERMISSION_RUN_TASKS + // NotEnabled: Tasker is not enabled + // AccessBlocked: user prefs disallow external access + // NoReceiver: Tasker has not created a listener for external access (probably a Tasker bug) + // OK: you should be able to send a task to run. Still need to listen for result + // for e.g. task not found + + public static enum Status { NotInstalled, NoPermission, NotEnabled, AccessBlocked, NoReceiver, OK }; + + // -------------------------- PRIVATE VARS ---------------------------- // + + private final static String TAG = "TaskerIntent"; + + private final static String EXTRA_INTENT_VERSION_NUMBER = "version_number"; + private final static String INTENT_VERSION_NUMBER = "1.1"; + + // Inclusive values + private final static int MIN_PRIORITY = 0; + private final static int MAX_PRIORITY = 10; + + // For generating random names + private static Random rand = new Random(); + + // Tracking state + private int actionCount = 0; + private int argCount; + + // -------------------------- PUBLIC METHODS ---------------------------- // + + public static int getMaxPriority() { + return MAX_PRIORITY; + } + + public static boolean validatePriority( int pri ) { + return ( + ( pri >= MIN_PRIORITY ) || + ( pri <= MAX_PRIORITY ) + ); + } + + // Tasker has different package names for Play Store and non- versions + // for historical reasons + + public static String getInstalledTaskerPackage( Context context ) { + + String foundPackage = null; + + try { + context.getPackageManager().getPackageInfo( TASKER_PACKAGE, 0 ); + foundPackage = TASKER_PACKAGE; + } + catch ( PackageManager.NameNotFoundException e ) { + } + + try { + context.getPackageManager().getPackageInfo( TASKER_PACKAGE_MARKET, 0 ); + foundPackage = TASKER_PACKAGE_MARKET; + } + catch ( PackageManager.NameNotFoundException e ) { + } + + return foundPackage; + } + + // test we can send a TaskerIntent to Tasker + // use *before* sending an intent + // still need to test the *result after* sending intent + + public static Status testStatus( Context c ) { + + Status result; + + if ( ! taskerInstalled( c ) ) + result = Status.NotInstalled; + else if ( ! havePermission( c ) ) + result = Status.NoPermission; + else if ( ! TaskerIntent.prefSet( c, PROVIDER_COL_NAME_ENABLED ) ) + result = Status.NotEnabled; + else if ( ! TaskerIntent.prefSet( c, PROVIDER_COL_NAME_EXTERNAL_ACCESS ) ) + result = Status.AccessBlocked; + else if ( ! new TaskerIntent( "" ).receiverExists( c ) ) + result = Status.NoReceiver; + else + result = Status.OK; + + return result; + } + + // Check if Tasker installed + + public static boolean taskerInstalled( Context context ) { + return ( getInstalledTaskerPackage( context ) != null ); + } + + // Use with startActivity to retrieve Tasker from Android market + public static Intent getTaskerInstallIntent( boolean marketFlag ) { + + return new Intent( + Intent.ACTION_VIEW, + Uri.parse( + marketFlag ? + ( ( SDKVersion() == CUPCAKE_SDK_VERSION ) ? TASKER_MARKET_URL_CUPCAKE : TASKER_MARKET_URL ) : + TASKER_DOWNLOAD_URL + ) + ); + } + + public static int SDKVersion() { + try { + Field f = android.os.Build.VERSION.class.getField( "SDK_INT" ); + return f.getInt( null ); + } + catch ( Exception e ) { + return CUPCAKE_SDK_VERSION; + } + } + + public static IntentFilter getCompletionFilter( String taskName ) { + + IntentFilter filter = new IntentFilter( TaskerIntent.ACTION_TASK_COMPLETE ); + + filter.addDataScheme( TASK_NAME_DATA_SCHEME ); + filter.addDataPath( taskName, PatternMatcher.PATTERN_LITERAL ); + + return filter; + } + + public static Intent getTaskSelectIntent() { + return new Intent( ACTION_TASK_SELECT ). + setFlags( + Intent.FLAG_ACTIVITY_NO_USER_ACTION | + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | + Intent.FLAG_ACTIVITY_NO_HISTORY + ); + } + + // public access deprecated, use TaskerIntent.testSend() instead + + public static boolean havePermission( Context c ) { + return c.checkPermission( PERMISSION_RUN_TASKS, Process.myPid(), Process.myUid() ) == + PackageManager.PERMISSION_GRANTED; + } + + // Get an intent that will bring up the Tasker prefs screen with the External Access control(s) + // Probably you want to use startActivity or startActivityForResult with it + + public static Intent getExternalAccessPrefsIntent() { + return new Intent( ACTION_OPEN_PREFS ).putExtra( EXTRA_OPEN_PREFS_TAB_NO, MISC_PREFS_TAB_NO ); + } + + // ------------------------------------- INSTANCE METHODS ----------------------------- // + + public TaskerIntent() { + super( ACTION_TASK ); + setRandomData(); + putMetaExtras( getRandomString() ); + } + + public TaskerIntent( String taskName ) { + super( ACTION_TASK ); + setRandomData(); + putMetaExtras( taskName ); + } + + public TaskerIntent setTaskPriority( int priority ) { + + if ( validatePriority( priority ) ) + putExtra( EXTRA_TASK_PRIORITY, priority ); + else + Log.e( TAG, "priority out of range: " + MIN_PRIORITY + ":" + MAX_PRIORITY ); + + return this; + } + + // Sets subsequently %par1, %par2 etc + public TaskerIntent addParameter( String value ) { + + int index = 1; + + if ( getExtras().containsKey( EXTRA_VAR_NAMES_LIST ) ) + index = getExtras().getStringArrayList( EXTRA_VAR_NAMES_LIST ).size() + 1; + + Log.d(TAG, "index: " + index ); + + addLocalVariable( "%" + PARAM_VAR_NAME_PREFIX + index, value ); + + return this; + } + + // Arbitrary specification of (local) variable names and values + public TaskerIntent addLocalVariable( String name, String value ) { + + ArrayList names, values; + + if ( hasExtra( EXTRA_VAR_NAMES_LIST ) ) { + names = getStringArrayListExtra( EXTRA_VAR_NAMES_LIST ); + values = getStringArrayListExtra( EXTRA_VAR_VALUES_LIST ); + } + else { + names = new ArrayList(); + values = new ArrayList(); + + putStringArrayListExtra( EXTRA_VAR_NAMES_LIST, names ); + putStringArrayListExtra( EXTRA_VAR_VALUES_LIST, values ); + } + + names.add( name ); + values.add( value ); + + return this; + } + + public TaskerIntent addAction( int code ) { + + actionCount++; + argCount = 1; + + Bundle actionBundle = new Bundle(); + + actionBundle.putInt( ACTION_CODE, code ); + + // Add action bundle to intent + putExtra( EXTRA_ACTION_INDEX_PREFIX + Integer.toString( actionCount ), actionBundle ); + + return this; + } + + // string arg + public TaskerIntent addArg( String arg ) { + + Bundle b = getActionBundle(); + + if ( b != null ) + b.putString( ARG_INDEX_PREFIX + Integer.toString( argCount++ ), arg ); + + return this; + } + + // int arg + public TaskerIntent addArg( int arg ) { + Bundle b = getActionBundle(); + + if ( b != null ) + b.putInt( ARG_INDEX_PREFIX + Integer.toString( argCount++ ), arg ); + + return this; + } + + // boolean arg + public TaskerIntent addArg( boolean arg ) { + Bundle b = getActionBundle(); + + if ( b != null ) + b.putBoolean( ARG_INDEX_PREFIX + Integer.toString( argCount++ ), arg ); + + return this; + } + + // Application arg + public TaskerIntent addArg( String pkg, String cls ) { + Bundle b = getActionBundle(); + + if ( b != null ) { + StringBuilder builder = new StringBuilder(); + builder.append( APP_ARG_PREFIX ). + append( pkg ). append( "," ). append( cls ); + b.putString( ARG_INDEX_PREFIX + Integer.toString( argCount++ ), builder.toString() ); + } + + return this; + } + + public IntentFilter getCompletionFilter() { + return getCompletionFilter( getTaskName() ); + } + + public String getTaskName() { + return getStringExtra( EXTRA_TASK_NAME ); + } + + public boolean receiverExists( Context context ) { + List recs = context.getPackageManager().queryBroadcastReceivers( this, 0 ); + return ( + ( recs != null ) && + ( recs.size() > 0 ) + ); + } + + // -------------------- PRIVATE METHODS -------------------- // + + private String getRandomString() { + return Long.toString( rand.nextLong() ); + } + + // so that if multiple TaskerIntents are used in PendingIntents there's virtually no + // clash chance + private void setRandomData() { + setData( Uri.parse( TASK_ID_SCHEME + ":" + getRandomString() ) ); + } + + private Bundle getActionBundle() { + + Bundle toReturn = null; + + if ( argCount > MAX_NO_ARGS ) + Log.e( TAG, "maximum number of arguments exceeded (" + MAX_NO_ARGS + ")" ); + else { + String key = EXTRA_ACTION_INDEX_PREFIX + Integer.toString( actionCount ); + + if ( this.hasExtra( key ) ) + toReturn = getBundleExtra( key ); + else + Log.e( TAG, "no actions added yet" ); + } + + return toReturn; + } + + private void putMetaExtras( String taskName ) { + putExtra( EXTRA_INTENT_VERSION_NUMBER, INTENT_VERSION_NUMBER ); + putExtra( EXTRA_TASK_NAME, taskName ); + } + + // for testing that Tasker is enabled and external access is allowed + + private static boolean prefSet( Context context, String col ) { + + String [] proj = new String [] { col }; + + Cursor c = context.getContentResolver().query( Uri.parse( TASKER_PREFS_URI ), proj, null, null, null ); + + boolean acceptingFlag = false; + + if ( c == null ) + Log.w( TAG, "no cursor for " + TASKER_PREFS_URI ); + else { + c.moveToFirst(); + + if ( Boolean.TRUE.toString().equals( c.getString( 0 ) ) ) + acceptingFlag = true; + + c.close(); + } + + return acceptingFlag; + } +} \ No newline at end of file diff --git a/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java b/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java new file mode 100644 index 00000000..d24b0a70 --- /dev/null +++ b/taskerpluginlibrary/src/main/java/net/dinglisch/android/tasker/TaskerPlugin.java @@ -0,0 +1,1005 @@ +//package com.yourcompany.yourcondition; +//package com.yourcompany.yoursetting; +package net.dinglisch.android.tasker; + +// Constants and functions for Tasker *extensions* to the plugin protocol +// See Also: http://tasker.dinglisch.net/plugins.html + +// Release Notes + +// v1.1 20140202 +// added function variableNameValid() +// fixed some javadoc entries (thanks to David Stone) + +// v1.2 20140211 +// added ACTION_EDIT_EVENT + +// v1.3 20140227 +// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER +// requestTimeoutMS(): added range check + +// v1.4 20140516 +// support for data pass through in REQUEST_QUERY intent +// some javadoc entries fixed (thanks again David :-)) + +// v1.5 20141120 +// added RESULT_CODE_FAILED_PLUGIN_FIRST +// added Setting.VARNAME_ERROR_MESSAGE + +// v1.6 20150213 +// added Setting.getHintTimeoutMS() +// added Host.addHintTimeoutMS() + +// v1.7 20160619 +// null check for getCallingActivity() in hostSupportsOnFireVariableReplacement( Activity editActivity ) + +// v1.8 20161002 +// added hostSupportsKeyEncoding(), setKeyEncoding() and Host.getKeysWithEncoding() + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.util.Log; + +import java.net.URISyntaxException; +import java.security.SecureRandom; +import java.util.regex.Pattern; + +public class TaskerPlugin { + + public final static String VARIABLE_PREFIX = "%"; + /** + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + public final static int EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2; + /** + * @see Condition#hostSupportsVariableReturn(Bundle) + */ + public final static int EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4; + /** + * @see Setting#hostSupportsOnFireVariableReplacement(Bundle) + */ + public final static int EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8; + public final static int EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32; + public final static int EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64; + public final static int EXTRA_HOST_CAPABILITY_ENCODING_JSON = 128; + private final static String TAG = "TaskerPlugin"; + private final static String BASE_KEY = "net.dinglisch.android.tasker"; + /** + * Action that the EditActivity for an event plugin should be launched by + */ + public final static String ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT"; + private final static String EXTRAS_PREFIX = BASE_KEY + ".extras."; + private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80; + // when generating non-repeating integers, look this far back for repeats + // see getPositiveNonRepeatingRandomInteger() + private final static int RANDOM_HISTORY_SIZE = 100; + private final static String VARIABLE_NAME_START_EXPRESSION = "[\\w&&[^_]]"; + private final static String VARIABLE_NAME_MID_EXPRESSION = "[\\w0-9]+"; + private final static String VARIABLE_NAME_END_EXPRESSION = "[\\w0-9&&[^_]]"; + public final static String VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION = + VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION; + public final static String VARIABLE_NAME_MATCH_EXPRESSION = + VARIABLE_PREFIX + "+" + + VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION; + /** + * @see #addVariableBundle(Bundle, Bundle) + * @see Host#getVariablesBundle(Bundle) + */ + private final static String EXTRA_VARIABLES_BUNDLE = EXTRAS_PREFIX + "VARIABLES"; + /** + * Host capabilities, passed to plugin with edit intents + */ + private final static String EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES"; + /** + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16; + public final static int EXTRA_HOST_CAPABILITY_ALL = + EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES | + EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES | + EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT | + EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES | + EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION | + EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH | + EXTRA_HOST_CAPABILITY_ENCODING_JSON; + private final static String BUNDLE_KEY_ENCODING_JSON_KEYS = BASE_KEY + ".JSON_ENCODED_KEYS"; + /** + * Miscellaneous operational hints going one way or the other + * + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + + private final static String EXTRA_HINTS_BUNDLE = EXTRAS_PREFIX + "HINTS"; + + ; + private final static String BUNDLE_KEY_HINT_PREFIX = ".hints."; + private final static String BUNDLE_KEY_HINT_TIMEOUT_MS = BUNDLE_KEY_HINT_PREFIX + "TIMEOUT"; + /** + * @see #hostSupportsRelevantVariables(Bundle) + * @see #addRelevantVariableList(Intent, String[]) + * @see #getRelevantVariableList(Bundle) + */ + private final static String BUNDLE_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES"; + private static Pattern VARIABLE_NAME_MATCH_PATTERN = null; + // state tracking for random number sequence + private static int[] lastRandomsSeen = null; + private static int randomInsertPointer = 0; + private static SecureRandom sr = null; + + public static boolean hostSupportsKeyEncoding(Bundle extrasFromHost, Encoding encoding) { + switch (encoding) { + case JSON: + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_ENCODING_JSON); + default: + return false; + } + } + + public static boolean hostSupportsRelevantVariables(Bundle extrasFromHost) { + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES); + } + + /** + * Specifies to host which variables might be used by the plugin. + *

+ * Used in EditActivity, before setResult(). + * + * @param intentToHost the intent being returned to the host + * @param variableNames array of relevant variable names + */ + public static void addRelevantVariableList(Intent intentToHost, String[] variableNames) { + intentToHost.putExtra(BUNDLE_KEY_RELEVANT_VARIABLES, variableNames); + } + + /** + * Validate a variable name. + *

+ * The basic requirement for variables from a plugin is that they must be all lower-case. + * + * @param varName name to check + */ + public static boolean variableNameValid(String varName) { + + boolean validFlag = false; + + if (varName == null) + Log.d(TAG, "variableNameValid: null name"); + else { + if (VARIABLE_NAME_MATCH_PATTERN == null) + VARIABLE_NAME_MATCH_PATTERN = Pattern.compile(VARIABLE_NAME_MATCH_EXPRESSION, 0); + + if (VARIABLE_NAME_MATCH_PATTERN.matcher(varName).matches()) { + + if (variableNameIsLocal(varName)) + validFlag = true; + else + Log.d(TAG, "variableNameValid: name not local: " + varName); + } else + Log.d(TAG, "variableNameValid: invalid name: " + varName); + } + + return validFlag; + } + + /** + * Allows the plugin/host to indicate to each other a set of variables which they are referencing. + * The host may use this to e.g. show a variable selection list in it's UI. + * The host should use this if it previously indicated to the plugin that it supports relevant vars + * + * @param fromHostIntentExtras usually from getIntent().getExtras() + * @return variableNames an array of relevant variable names + */ + public static String[] getRelevantVariableList(Bundle fromHostIntentExtras) { + + String[] relevantVars = (String[]) getBundleValueSafe(fromHostIntentExtras, BUNDLE_KEY_RELEVANT_VARIABLES, String[].class, "getRelevantVariableList"); + + if (relevantVars == null) + relevantVars = new String[0]; + + return relevantVars; + } + + // ----------------------------- SETTING PLUGIN ONLY --------------------------------- // + + /** + * Used by: plugin QueryReceiver, FireReceiver + *

+ * Add a bundle of variable name/value pairs. + *

+ * Names must be valid Tasker local variable names. + * Values must be String, String [] or ArrayList + * Null values cause deletion of possible already-existing variables + * A null value where the variable does not already exist results in attempted deletion + * of any existing array indices (%arr1, %arr2 etc) + * + * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras()) + * @param variables the variables to send + * @see Setting#hostSupportsVariableReturn(Bundle) + * @see #variableNameValid(String) + */ + public static void addVariableBundle(Bundle resultExtras, Bundle variables) { + resultExtras.putBundle(EXTRA_VARIABLES_BUNDLE, variables); + } + + // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- // + + /** + * Used by: plugin EditActivity + *

+ * Specify the encoding for a set of bundle keys. + *

+ * This is completely optional and currently only necessary if using Setting#setVariableReplaceKeys + * where the corresponding values of some of the keys specified are JSON encoded. + * + * @param resultBundleToHost the bundle being returned to the host + * @param keys the keys being returned to the host which are encoded in some way + * @param encoding the encoding of the values corresponding to the specified keys + * @see #setVariableReplaceKeys(Bundle, String[]) + * @see #hostSupportsKeyEncoding(Bundle, Encoding) + */ + public static void setKeyEncoding(Bundle resultBundleToHost, String[] keys, Encoding encoding) { + if (Encoding.JSON.equals(encoding)) + addStringArrayToBundleAsString( + keys, resultBundleToHost, BUNDLE_KEY_ENCODING_JSON_KEYS, "setValueEncoding" + ); + else + Log.e(TAG, "unknown encoding: " + encoding); + } + + // ----------------------------- EVENT PLUGIN ONLY --------------------------------- // + + private static Object getBundleValueSafe(Bundle b, String key, Class expectedClass, String funcName) { + Object value = null; + + if (b != null) { + if (b.containsKey(key)) { + Object obj = b.get(key); + if (obj == null) + Log.w(TAG, funcName + ": " + key + ": null value"); + else if (obj.getClass() != expectedClass) + Log.w(TAG, funcName + ": " + key + ": expected " + expectedClass.getClass().getName() + ", got " + obj.getClass().getName()); + else + value = obj; + } + } + return value; + } + // ---------------------------------- HOST ----------------------------------------- // + + private static Object getExtraValueSafe(Intent i, String key, Class expectedClass, String funcName) { + return (i.hasExtra(key)) ? + getBundleValueSafe(i.getExtras(), key, expectedClass, funcName) : + null; + } + + // ---------------------------------- HELPER FUNCTIONS -------------------------------- // + + private static boolean hostSupports(Bundle extrasFromHost, int capabilityFlag) { + Integer flags = (Integer) getBundleValueSafe(extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports"); + return + (flags != null) && + ((flags & capabilityFlag) > 0) + ; + } + + public static int getPackageVersionCode(PackageManager pm, String packageName) { + + int code = -1; + + if (pm != null) { + try { + PackageInfo pi = pm.getPackageInfo(packageName, 0); + if (pi != null) + code = pi.versionCode; + } catch (Exception e) { + Log.e(TAG, "getPackageVersionCode: exception getting package info"); + } + } + + return code; + } + + private static boolean variableNameIsLocal(String varName) { + + int digitCount = 0; + int length = varName.length(); + + for (int x = 0; x < length; x++) { + char ch = varName.charAt(x); + + if (Character.isUpperCase(ch)) + return false; + else if (Character.isDigit(ch)) + digitCount++; + } + + if (digitCount == (varName.length() - 1)) + return false; + + return true; + } + + private static String[] getStringArrayFromBundleString(Bundle bundle, String key, String funcName) { + + String spec = (String) getBundleValueSafe(bundle, key, String.class, funcName); + + String[] toReturn = null; + + if (spec != null) + toReturn = spec.split(" "); + + return toReturn; + } + + private static void addStringArrayToBundleAsString(String[] toAdd, Bundle bundle, String key, String callerName) { + + StringBuilder builder = new StringBuilder(); + + if (toAdd != null) { + + for (String keyName : toAdd) { + + if (keyName.contains(" ")) + Log.w(TAG, callerName + ": ignoring bad keyName containing space: " + keyName); + else { + if (builder.length() > 0) + builder.append(' '); + + builder.append(keyName); + } + + if (builder.length() > 0) + bundle.putString(key, builder.toString()); + } + } + } + + /** + * Generate a sequence of secure random positive integers which is guaranteed not to repeat + * in the last 100 calls to this function. + * + * @return a random positive integer + */ + public static int getPositiveNonRepeatingRandomInteger() { + + // initialize on first call + if (sr == null) { + sr = new SecureRandom(); + lastRandomsSeen = new int[RANDOM_HISTORY_SIZE]; + + for (int x = 0; x < lastRandomsSeen.length; x++) + lastRandomsSeen[x] = -1; + } + + int toReturn; + do { + // pick a number + toReturn = sr.nextInt(Integer.MAX_VALUE); + + // check we havn't see it recently + for (int seen : lastRandomsSeen) { + if (seen == toReturn) { + toReturn = -1; + break; + } + } + } + while (toReturn == -1); + + // update history + lastRandomsSeen[randomInsertPointer] = toReturn; + randomInsertPointer = (randomInsertPointer + 1) % lastRandomsSeen.length; + + return toReturn; + } + + /** + * Possible encodings of text in bundle values + * + * @see #setKeyEncoding(Bundle, String[], Encoding) + */ + public enum Encoding { + JSON + } + + public static class Setting { + + /** + * Variable name into which a description of any error that occurred can be placed + * for the user to process. + *

+ * Should *only* be set when the BroadcastReceiver result code indicates a failure. + *

+ * Note that the user needs to have configured the task to continue after failure of the plugin + * action otherwise they will not be able to make use of the error message. + *

+ * For use with #addRelevantVariableList(Intent, String[]) and #addVariableBundle(Bundle, Bundle) + */ + public final static String VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg"; + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + + public final static int REQUESTED_TIMEOUT_MS_NONE = 0; + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + + public final static int REQUESTED_TIMEOUT_MS_MAX = 3599000; + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + + public final static int REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000; + /** + * @see #signalFinish(Context, Intent, int, Bundle) + * @see Host#getSettingResultCode(Intent) + */ + public final static String EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE"; + /** + * @see #signalFinish(Context, Intent, int, Bundle) + * @see Host#getSettingResultCode(Intent) + */ + + public final static int RESULT_CODE_OK = Activity.RESULT_OK; + public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER; + public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1; + public final static int RESULT_CODE_PENDING = Activity.RESULT_FIRST_USER + 2; + public final static int RESULT_CODE_UNKNOWN = Activity.RESULT_FIRST_USER + 3; + /** + * If a plugin wants to define it's own error codes, start numbering them here. + * The code will be placed in an error variable (%err in the case of Tasker) for + * the user to process after the plugin action. + */ + + public final static int RESULT_CODE_FAILED_PLUGIN_FIRST = Activity.RESULT_FIRST_USER + 9; + public final static String EXTRA_CALL_SERVICE_PACKAGE = TaskerIntent.TASKER_PACKAGE + ".EXTRA_CALL_SERVICE_PACKAGE"; + public final static String EXTRA_CALL_SERVICE = TaskerIntent.TASKER_PACKAGE + ".EXTRA_CALL_SERVICE"; + public final static String EXTRA_CALL_SERVICE_FOREGROUND = TaskerIntent.TASKER_PACKAGE + ".EXTRA_CALL_SERVICE_FOREGROUND"; + public final static String EXTRA_RESULT_CALL_URI = BASE_KEY + ".EXTRA_RESULT_CALL_URI"; + /** + * @see #setVariableReplaceKeys(Bundle, String[]) + */ + private final static String BUNDLE_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS"; + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + private final static String EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT"; + /** + * @see #signalFinish(Context, Intent, int, Bundle) + * @see Host#addCompletionIntent(Intent, Intent, boolean) + */ + private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT"; + + /** + * Used by: plugin EditActivity. + *

+ * Indicates to plugin that host will replace variables in specified bundle keys. + *

+ * Replacement takes place every time the setting is fired, before the bundle is + * passed to the plugin FireReceiver. + * + * @param extrasFromHost intent extras from the intent received by the edit activity + * @see #setVariableReplaceKeys(Bundle, String[]) + */ + public static boolean hostSupportsOnFireVariableReplacement(Bundle extrasFromHost) { + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT); + } + + /** + * Used by: plugin EditActivity. + *

+ * Description as above. + *

+ * This version also includes backwards compatibility with pre 4.2 Tasker versions. + * At some point this function will be deprecated. + * + * @param editActivity the plugin edit activity, needed to test calling Tasker version + * @see #setVariableReplaceKeys(Bundle, String[]) + */ + + public static boolean hostSupportsOnFireVariableReplacement(Activity editActivity) { + + boolean supportedFlag = hostSupportsOnFireVariableReplacement(editActivity.getIntent().getExtras()); + + if (!supportedFlag) { + + ComponentName callingActivity = editActivity.getCallingActivity(); + + if (callingActivity == null) + Log.w(TAG, "hostSupportsOnFireVariableReplacement: null callingActivity, defaulting to false"); + else { + String callerPackage = callingActivity.getPackageName(); + + // Tasker only supporteed this from 1.0.10 + supportedFlag = + (callerPackage.startsWith(BASE_KEY)) && + (getPackageVersionCode(editActivity.getPackageManager(), callerPackage) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION) + ; + } + } + + return supportedFlag; + } + + public static boolean hostSupportsSynchronousExecution(Bundle extrasFromHost) { + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION); + } + + /** + * Request the host to wait the specified number of milliseconds before continuing. + * Note that the host may choose to ignore the request. + *

+ * Maximum value is REQUESTED_TIMEOUT_MS_MAX. + * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting + * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for + * a result). + *

+ * Used in EditActivity, before setResult(). + * + * @param intentToHost the intent being returned to the host + * @param timeoutMS + */ + public static void requestTimeoutMS(Intent intentToHost, int timeoutMS) { + if (timeoutMS < 0) + Log.w(TAG, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")"); + else { + if ( + (timeoutMS > REQUESTED_TIMEOUT_MS_MAX) && + (timeoutMS != REQUESTED_TIMEOUT_MS_NEVER) + ) { + Log.w(TAG, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")"); + timeoutMS = REQUESTED_TIMEOUT_MS_MAX; + } + intentToHost.putExtra(EXTRA_REQUESTED_TIMEOUT, timeoutMS); + } + } + + /** + * Used by: plugin EditActivity + *

+ * Indicates to host which bundle keys should be replaced. + * + * @param resultBundleToHost the bundle being returned to the host + * @param listOfKeyNames which bundle keys to replace variables in when setting fires + * @see #hostSupportsOnFireVariableReplacement(Bundle) + * @see #setKeyEncoding(Bundle, String[], Encoding) + */ + public static void setVariableReplaceKeys(Bundle resultBundleToHost, String[] listOfKeyNames) { + addStringArrayToBundleAsString( + listOfKeyNames, resultBundleToHost, BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, + "setVariableReplaceKeys" + ); + } + + /** + * Used by: plugin FireReceiver + *

+ * Indicates to plugin whether the host will process variables which it passes back + * + * @param extrasFromHost intent extras from the intent received by the FireReceiver + * @see #signalFinish(Context, Intent, int, Bundle) + */ + public static boolean hostSupportsVariableReturn(Bundle extrasFromHost) { + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES); + } + + public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars ) { + return signalFinish(context, originalFireIntent, resultCode, vars,null); + } + /** + * Used by: plugin FireReceiver + * + * Tell the host that the plugin has finished execution. + * + * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive(). + * + * @param originalFireIntent the intent received from the host (via onReceive()) + * @param resultCode level of success in performing the settings + * @param vars any variables that the plugin wants to set in the host + * @see #hostSupportsSynchronousExecution(Bundle) + */ + public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars, Uri callbackUri ) { + + String errorPrefix = "signalFinish: "; + + boolean okFlag = false; + + String completionIntentString = (String) getExtraValueSafe(originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish"); + + if (completionIntentString != null) { + + Uri completionIntentUri = null; + try { + completionIntentUri = Uri.parse(completionIntentString); + } + // should only throw NullPointer but don't particularly trust it + catch (Exception e) { + Log.w(TAG, errorPrefix + "couldn't parse " + completionIntentString); + } + + if (completionIntentUri != null) { + try { + Intent completionIntent = Intent.parseUri(completionIntentString, Intent.URI_INTENT_SCHEME); + + completionIntent.putExtra(EXTRA_RESULT_CODE, resultCode); + + if (vars != null) + completionIntent.putExtra(EXTRA_VARIABLES_BUNDLE, vars); + + if(callbackUri != null){ + completionIntent.putExtra(EXTRA_RESULT_CALL_URI, callbackUri); + } + String callServicePackage = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_PACKAGE, String.class, "signalFinish"); + String callService = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE, String.class, "signalFinish"); + Boolean foreground = (Boolean) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_FOREGROUND, Boolean.class, "signalFinish"); + if (callServicePackage != null && callService != null && foreground != null) { + completionIntent.setComponent(new ComponentName(callServicePackage, callService)); + if (foreground && android.os.Build.VERSION.SDK_INT >= 26) { + context.startForegroundService(completionIntent); + } else { + context.startService(completionIntent); + } + } else { + context.sendBroadcast(completionIntent); + } + + okFlag = true; + } catch (URISyntaxException e) { + Log.w(TAG, errorPrefix + "bad URI: " + completionIntentUri); + } catch (IllegalStateException e) { + Log.w(TAG, errorPrefix + "host was not in foreground: " + completionIntentUri,e); + } + } + } + + return okFlag; + } + + /** + * Check for a hint on the timeout value the host is using. + * Used by: plugin FireReceiver. + * Requires Tasker 4.7+ + * + * @param extrasFromHost intent extras from the intent received by the FireReceiver + * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available. + * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER + */ + public static int getHintTimeoutMS(Bundle extrasFromHost) { + + int timeoutMS = -1; + + Bundle hintsBundle = (Bundle) TaskerPlugin.getBundleValueSafe(extrasFromHost, EXTRA_HINTS_BUNDLE, Bundle.class, "getHintTimeoutMS"); + + if (hintsBundle != null) { + + Integer val = (Integer) getBundleValueSafe(hintsBundle, BUNDLE_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS"); + + if (val != null) + timeoutMS = val; + } + + return timeoutMS; + } + } + + public static class Condition { + + /** + * @see #getResultReceiver(Intent) + */ + public final static String EXTRA_RESULT_RECEIVER = TaskerIntent.TASKER_PACKAGE + ".EXTRA_RESULT_RECEIVER"; + + /** + * Used by: plugin QueryReceiver + *

+ * Indicates to plugin whether the host will process variables which it passes back + * + * @param extrasFromHost intent extras from the intent received by the QueryReceiver + * @see #addVariableBundle(Bundle, Bundle) + */ + public static boolean hostSupportsVariableReturn(Bundle extrasFromHost) { + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES); + } + + + public static ResultReceiver getResultReceiver(Intent intentFromHost) { + if (intentFromHost == null) { + return null; + } + return (ResultReceiver) getExtraValueSafe(intentFromHost, EXTRA_RESULT_RECEIVER, ResultReceiver.class, "getResultReceiver"); + + } + } + + public static class Event { + + public final static String PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID"; + + private final static String EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA"; + + /** + * @param extrasFromHost intent extras from the intent received by the QueryReceiver + * @see #addPassThroughData(Intent, Bundle) + */ + public static boolean hostSupportsRequestQueryDataPassThrough(Bundle extrasFromHost) { + return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH); + } + + /** + * Specify a bundle of data (probably representing whatever change happened in the condition) + * which will be included in the QUERY_CONDITION broadcast sent by the host for each + * event instance of the plugin. + *

+ * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the + * with the REQUEST_QUERY that caused it. + *

+ * Note that for security reasons it is advisable to also store a message ID with the bundle + * which can be compared to known IDs on receipt. The host cannot validate the source of + * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible. + * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's + * own ID to the pass through bundle. + *

+ * Note also that there are several situations where REQUEST_QUERY will not result in a + * QUERY_CONDITION intent (e.g. event throttling by the host), so plugin-local data + * indexed with a message ID needs to be timestamped and eventually timed-out. + *

+ * This function can be called multiple times, each time all keys in data will be added to + * that of previous calls. + * + * @param requestQueryIntent intent being sent to the host + * @param data the data to be passed-through + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #retrievePassThroughData(Intent) + * @see #addPassThroughMessageID + */ + public static void addPassThroughData(Intent requestQueryIntent, Bundle data) { + + Bundle passThroughBundle = retrieveOrCreatePassThroughBundle(requestQueryIntent); + + passThroughBundle.putAll(data); + } + + /** + * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated + * by a REQUEST_QUERY from the plugin. + *

+ * Note that if addPassThroughMessageID() was previously called, the data will contain an extra + * key TaskerPlugin.Event.PASS_THOUGH_BUNDLE_MESSAGE_ID_KEY. + * + * @param queryConditionIntent QUERY_REQUEST sent from host + * @return data previously added to the REQUEST_QUERY intent + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #addPassThroughData(Intent, Bundle) + */ + public static Bundle retrievePassThroughData(Intent queryConditionIntent) { + return (Bundle) getExtraValueSafe( + queryConditionIntent, + EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, + Bundle.class, + "retrievePassThroughData" + ); + } + + /** + * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding + * QUERY_CONDITION broadcast sent by the host for each event instance of the plugin. + *

+ * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the + * with the REQUEST_QUERY that caused it. It also allows the message to be verified + * by the plugin to prevent e.g. replay attacks + * + * @param requestQueryIntent intent being sent to the host + * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #retrievePassThroughData(Intent) + */ + public static int addPassThroughMessageID(Intent requestQueryIntent) { + + Bundle passThroughBundle = retrieveOrCreatePassThroughBundle(requestQueryIntent); + + int id = getPositiveNonRepeatingRandomInteger(); + + passThroughBundle.putInt(PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, id); + + return id; + } + + /* + * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated + * by a REQUEST_QUERY from the plugin. + * + * @param queryConditionIntent QUERY_REQUEST sent from host + * @return the ID which was passed through by the host, or -1 if no ID was found + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #addPassThroughData(Intent,Bundle) + */ + public static int retrievePassThroughMessageID(Intent queryConditionIntent) { + + int toReturn = -1; + + Bundle passThroughData = Event.retrievePassThroughData(queryConditionIntent); + + if (passThroughData != null) { + Integer id = (Integer) getBundleValueSafe( + passThroughData, + PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, + Integer.class, + "retrievePassThroughMessageID" + ); + + if (id != null) + toReturn = id; + } + + return toReturn; + } + + // internal use + private static Bundle retrieveOrCreatePassThroughBundle(Intent requestQueryIntent) { + + Bundle passThroughBundle; + + if (requestQueryIntent.hasExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA)) + passThroughBundle = requestQueryIntent.getBundleExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA); + else { + passThroughBundle = new Bundle(); + requestQueryIntent.putExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughBundle); + } + + return passThroughBundle; + } + } + + public static class Host { + + /** + * Tell the plugin what capabilities the host support. This should be called when sending + * intents to any EditActivity, FireReceiver or QueryReceiver. + * + * @param toPlugin the intent we're sending + * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags + */ + public static Intent addCapabilities(Intent toPlugin, int capabilities) { + return toPlugin.putExtra(EXTRA_HOST_CAPABILITIES, capabilities); + } + + /** + * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin + * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true. + * + * @param fireIntent fire intent going to the plugin + * @param completionIntent intent which will signal the host that the plugin is finished. + * Implementation is host-dependent. + */ + public static void addCompletionIntent(Intent fireIntent, Intent completionIntent, ComponentName callService) { + if (callService != null) { + completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_PACKAGE, callService.getPackageName()); + completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE, callService.getClassName()); + completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_FOREGROUND, android.os.Build.VERSION.SDK_INT >= 26); + } + fireIntent.putExtra( + Setting.EXTRA_PLUGIN_COMPLETION_INTENT, + completionIntent.toUri(Intent.URI_INTENT_SCHEME) + ); + } + + /** + * When a setting plugin is finished, it sends the host the intent which was passed to it + * via @code{addCompletionIntent}. + * + * @param completionIntent intent returned from the plugin when it finished. + * @return resultCode measure of plugin success, defaults to UNKNOWN + */ + public static int getSettingResultCode(Intent completionIntent) { + + Integer val = (Integer) getExtraValueSafe(completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode"); + + return (val == null) ? Setting.RESULT_CODE_UNKNOWN : val; + } + + /** + * Extract a bundle of variables from an intent received from the FireReceiver. This + * should be called if the host previously indicated to the plugin + * that it supports setting variable return. + * + * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive() + * @return variables a bundle of variable name/value pairs + * @see #addCapabilities(Intent, int) + */ + + public static Bundle getVariablesBundle(Bundle resultExtras) { + return (Bundle) getBundleValueSafe( + resultExtras, EXTRA_VARIABLES_BUNDLE, Bundle.class, "getVariablesBundle" + ); + } + + /** + * Inform a setting plugin of the timeout value the host is using. + * + * @param toPlugin the intent we're sending + * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from + * that which the plugin requests. + * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER + */ + public static void addHintTimeoutMS(Intent toPlugin, int timeoutMS) { + getHintsBundle(toPlugin, "addHintTimeoutMS").putInt(BUNDLE_KEY_HINT_TIMEOUT_MS, timeoutMS); + } + + private static Bundle getHintsBundle(Intent intent, String funcName) { + + Bundle hintsBundle = (Bundle) getExtraValueSafe(intent, EXTRA_HINTS_BUNDLE, Bundle.class, funcName); + + if (hintsBundle == null) { + hintsBundle = new Bundle(); + intent.putExtra(EXTRA_HINTS_BUNDLE, hintsBundle); + } + + return hintsBundle; + } + + public static boolean haveRequestedTimeout(Bundle extrasFromPluginEditActivity) { + return extrasFromPluginEditActivity.containsKey(Setting.EXTRA_REQUESTED_TIMEOUT); + } + + public static int getRequestedTimeoutMS(Bundle extrasFromPluginEditActivity) { + return + (Integer) getBundleValueSafe( + extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT, Integer.class, "getRequestedTimeout" + ) + ; + } + + public static String[] getSettingVariableReplaceKeys(Bundle fromPluginEditActivity) { + return getStringArrayFromBundleString( + fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, + "getSettingVariableReplaceKeys" + ); + } + + public static String[] getKeysWithEncoding(Bundle fromPluginEditActivity, Encoding encoding) { + + String[] toReturn = null; + + if (Encoding.JSON.equals(encoding)) + toReturn = getStringArrayFromBundleString( + fromPluginEditActivity, TaskerPlugin.BUNDLE_KEY_ENCODING_JSON_KEYS, + "getKeyEncoding:JSON" + ); + else + Log.w(TAG, "Host.getKeyEncoding: unknown encoding " + encoding); + + return toReturn; + } + + public static boolean haveRelevantVariables(Bundle b) { + return b.containsKey(BUNDLE_KEY_RELEVANT_VARIABLES); + } + + public static void cleanRelevantVariables(Bundle b) { + b.remove(BUNDLE_KEY_RELEVANT_VARIABLES); + } + + public static void cleanHints(Bundle extras) { + extras.remove(TaskerPlugin.EXTRA_HINTS_BUNDLE); + } + + public static void cleanRequestedTimeout(Bundle extras) { + extras.remove(Setting.EXTRA_REQUESTED_TIMEOUT); + } + + public static void cleanSettingReplaceVariables(Bundle b) { + b.remove(Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS); + } + } + +} diff --git a/taskerpluginlibrary/src/main/res/mipmap/ic_launcher.png b/taskerpluginlibrary/src/main/res/mipmap/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ff10afd6e182edb2b1a63c8f984e9070d9f950ba GIT binary patch literal 2096 zcmV-02+#M4P)xsEBz6iy~SX+W%nrKL2KH{`gFsDCOB6ZW0@Yj?g&st+$-t|2c4&NM7M5Tk(z5p1+IN@y}=N)4$Vmgo_?Y@Ck5u}3=}@K z);Ns<{X)3-we^O|gm)Oh1^>hg6g=|b7E-r?H6QeeKvv7{-kP9)eb76lZ>I5?WDjiX z7Qu}=I4t9`G435HO)Jpt^;4t zottB%?uUE#zt^RaO&$**I5GbJM-Nj&Z#XT#=iLsG7*JO@)I~kH1#tl@P}J@i#`XX! zEUc>l4^`@w2_Fsoa*|Guk5hF2XJq0TQ{QXsjnJ)~K{EG*sHQW(a<^vuQkM07vtNw= z{=^9J-YI<#TM>DTE6u^^Z5vsVZx{Lxr@$j8f2PsXr^)~M97)OdjJOe81=H#lTbl`!5}35~o;+uSbUHP+6L00V99ox@t5JT2~=-{-Zvti4(UkQKDs{%?4V4AV3L`G476;|CgCH%rI z;0kA=z$nkcwu1-wIX=yE5wwUO)D;dT0m~o7z(f`*<1B>zJhsG0hYGMgQ0h>ylQYP; zbY|ogjI;7_P6BwI^6ZstC}cL&6%I8~cYe1LP)2R}amKG>qavWEwL0HNzwt@3hu-i0 z>tX4$uXNRX_<>h#Q`kvWAs3Y+9)i~VyAb3%4t+;Ej~o)%J#d6}9XXtC10QpHH*X!(vYjmZ zlmm6A=sN)+Lnfb)wzL90u6B=liNgkPm2tWfvU)a0y=N2gqg_uRzguCqXO<0 zp@5n^hzkW&E&~|ZnlPAz)<%Cdh;IgaTGMjVcP{dLFnX>K+DJ zd?m)lN&&u@soMY!B-jeeZNHfQIu7I&9N?AgMkXKxIC+JQibV=}9;p)91_6sP0x=oO zd9T#KhN9M8uO4rCDa ze;J+@sfk?@C6ke`KmkokKLLvbpNHGP^1^^YoBV^rxnXe8nl%NfKS}ea`^9weO&eZ` zo3Nb?%LfcmGM4c%PpK;~v#XWF+!|RaTd$6126a6)WGQPmv0E@fm9;I@#QpU0rcGEJ zNS_DL26^sx!>ccJF}F){`A0VIvLan^$?MI%g|@ebIFlrG&W$4|8=~H%Xsb{gawm(u zEgD&|uQgc{a;4k6J|qjRZzat^hbRSXZwu7(c-+?ku6G1X0c*0%*CyUsXxlKf=%wfS z7A!7+`^?MrPvs?yo31D=ZCu!3UU`+dR^S>@R%-y+!b$RlnflhseNn10MV5M=0KfZ+ zl9DEH0jK5}{VOgmzKClJ7?+=AED&7I=*K$;ONIUM3nyT|P}|NXn@Qhn<7H$I*mKw1 axPAxe%7rDusX+w*0000 + TaskerPluginLibrary + Tasker Plugin Service + Used to run Tasker actions and conditions + Running Tasker plugin... + Error Code + If there\'s an error, contains an error code + Error Message + If there\'s an error, contains an error message + Tasker plugin service that executes the app\'s actions from Tasker + Tasker plugin service that checks the app\'s conditions from Tasker + From b8e8a3ce058a4739b9828d30a6748a435276be65 Mon Sep 17 00:00:00 2001 From: Sauvio Date: Wed, 6 Dec 2023 00:16:20 +0800 Subject: [PATCH 2/7] feat: TaskerPlugin adds coordinate pointer information for text --- app/build.gradle | 2 +- .../github/subhamtyagi/ocr/tasker/OCRAPI.kt | 4 +- .../subhamtyagi/ocr/tasker/OCRConfig.kt | 59 ++++++++++++++++--- .../subhamtyagi/ocr/tasker/OCROutput.kt | 3 +- .../subhamtyagi/ocr/tasker/OCRRunner.kt | 7 +-- app/src/main/res/values-cn/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 61 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c7115fdd..8be846af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ android { defaultConfig { applicationId "io.github.subhamtyagi.ocr" - minSdkVersion 18 + minSdkVersion 19 targetSdkVersion 34 versionCode 12 versionName "4.0" diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt index f84aae82..f07bc796 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRAPI.kt @@ -1,4 +1,4 @@ -package io.github.subhamtyagi.ocr.tasker; +package io.github.subhamtyagi.ocr.tasker import android.content.Context import com.googlecode.tesseract.android.TessBaseAPI @@ -16,7 +16,7 @@ class TessBaseOCRFactory(private val context: Context) : OCRFactory{ val mLanguage = Utils.getTrainingDataLanguage() val mPageSegMode = Utils.getPageSegMode() val dir = File(context.getExternalFilesDir(Utils.getTrainingDataType().toString())!!.absolutePath) - val mEngineMode = Utils.getEngineMode(); + val mEngineMode = Utils.getEngineMode() val api = TessBaseAPI() api.init(dir.absolutePath, mLanguage, mEngineMode) api.pageSegMode = mPageSegMode diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt index 06a0f25c..e09809f8 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt @@ -3,8 +3,11 @@ package io.github.subhamtyagi.ocr.tasker import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import androidx.core.content.FileProvider +import com.googlecode.tesseract.android.TessBaseAPI +import com.googlecode.tesseract.android.TessBaseAPI.PageIteratorLevel import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelper import com.joaomgcd.taskerpluginlibrary.input.TaskerInput @@ -18,6 +21,8 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject import java.io.File @@ -33,27 +38,65 @@ class BackgroundWork: CoroutineScope by MainScope() { } } } - private fun readTextFromOCRAPI(context: Context, pathName: String): String { - pathName.ifEmpty { return "Scan Failed: Must choose image file!" } + private fun readTextFromOCRAPI(context: Context, pathName: String): OCROutput { + pathName.ifEmpty { return OCROutput("Scan Failed: Must choose image file!","") } val imageFile = File(pathName) val imageUri = FileProvider.getUriForFile(context, CropFileProvider.authority(context), imageFile) var bitmap = BitmapUtils.decodeSampledBitMap(context, imageUri)?.bitmap - // call image reader api - val result = TessBaseOCRFactory(context).let { + val output = TessBaseOCRFactory(context).let { val api = it.createAPI() if (Utils.isPreProcessImage()) bitmap = Utils.preProcessBitmap(bitmap) api.setImage(bitmap) val textOnImage = try { - api.utF8Text; + api.utF8Text } catch (e: Exception) { "Scan Failed: WTF: Must be reported to developer!" } textOnImage.ifEmpty { - "Scan Failed: Couldn't read the image\nProblem may be related to Tesseract or no Text on Image!" + OCROutput("Scan Failed: Couldn't read the image\nProblem may be related to Tesseract or no Text on Image!","") + } + val coordinatesData = assembleCoordinatesData(api) + OCROutput(textOnImage, coordinatesData) + } + return output + } + + /** + * coordinates with top-left of the top-left contained pixel + * to the bottom-right of the bottom-right contained pixel + * @return A map. Key for each individual symbol, value for an array of coordinates + */ + private fun readCoordinates(api: TessBaseAPI): Map> { + val coordinatesMap: MutableMap> = mutableMapOf() + val resultIterator = api.resultIterator + resultIterator.begin() + do { + val symbol = resultIterator.getUTF8Text(PageIteratorLevel.RIL_SYMBOL) + val boundingBox = resultIterator.getBoundingBox(PageIteratorLevel.RIL_SYMBOL) + + if (symbol.isNotBlank()) { + coordinatesMap[symbol] = arrayOf(boundingBox) } + + } while (resultIterator.next(PageIteratorLevel.RIL_SYMBOL)) + resultIterator.delete() + + return coordinatesMap + } + + /** + * Assemble the key-value information for the coordinates of each individual symbol. + * @return JSON string. eg:{"1":[[151,33,154,72]],"0":[[151,33,171,67]],"g":[[176,44,196,72]]} + */ + private fun assembleCoordinatesData(api: TessBaseAPI): String { + val coordinatesMap = readCoordinates(api) + val jsonObject = JSONObject() + coordinatesMap.forEach { (symbol, boundingBoxes) -> + jsonObject.put(symbol, JSONArray(boundingBoxes.map { JSONArray(it) })) } - return result + Log.d("json data","===>$jsonObject") + return jsonObject.toString() } } @@ -88,7 +131,7 @@ class ActivityConfigOCR : ActivityConfigTasker) = BackgroundHelper(config) diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt index 52d09795..b444a199 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCROutput.kt @@ -5,7 +5,8 @@ import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable @TaskerOutputObject() class OCROutput( - @get:TaskerOutputVariable(VAR_RESULT, labelResIdName = "result", htmlLabelResIdName = "result_description") var result: String? + @get:TaskerOutputVariable(VAR_RESULT, labelResIdName = "result", htmlLabelResIdName = "result_description") var result: String?, + @get:TaskerOutputVariable("coordinates", labelResIdName = "coordinates", htmlLabelResIdName = "coordinates_description") var coordinates: String ) { companion object { const val VAR_RESULT = "result" diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt index b298a17d..3aff4902 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRRunner.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking object ChannelManager { - val channelResult = Channel() + val channelResult = Channel() } const val TAG = "OCRRunner" class OCRRunner : TaskerPluginRunnerAction() { @@ -22,9 +22,8 @@ class OCRRunner : TaskerPluginRunnerAction() { override fun run(context: Context,input: TaskerInput): TaskerPluginResult { val imagePathName = input.regular.imagePathName?: "" val output = runBlocking { - BackgroundWork().readText(context, imagePathName); - val result = channelResult.receive() - OCROutput(result) + BackgroundWork().readText(context, imagePathName) + channelResult.receive() } return TaskerPluginResultSucess(output) } diff --git a/app/src/main/res/values-cn/strings.xml b/app/src/main/res/values-cn/strings.xml index adf9deb6..a4d09744 100644 --- a/app/src/main/res/values-cn/strings.xml +++ b/app/src/main/res/values-cn/strings.xml @@ -61,6 +61,7 @@ 选择或搜索语言 决定功能和速度 文字识别结果 + 每一个文字的坐标键值信息 应用存储空间下的图片绝对路径 存储文字识别结果的可选变量名 文字识别结果变量名 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a33c582e..2806663c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,4 +80,5 @@ Determines accuracy and efficiency OCR Engine mode The absolute path of the image in the application storage space + The key-value information for the coordinates of each individual character \ No newline at end of file From 945d77a3950626ed011b2e3efd796a2d6fe0e2d1 Mon Sep 17 00:00:00 2001 From: Sauvio Date: Wed, 6 Dec 2023 17:05:23 +0800 Subject: [PATCH 3/7] feat: Language setting fix: The engine mode setting is optimized --- .../github/subhamtyagi/ocr/MainActivity.java | 19 +++++++++++++ .../subhamtyagi/ocr/SettingsActivity.java | 16 ++++++++++- .../subhamtyagi/ocr/utils/Constants.java | 5 ++-- .../subhamtyagi/ocr/utils/LanguageUtil.java | 27 +++++++++++++++++++ app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-be/strings.xml | 1 + app/src/main/res/values-bn/strings.xml | 1 + app/src/main/res/values-cn/arrays.xml | 7 +++++ app/src/main/res/values-cn/strings.xml | 3 +++ app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-he/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-id/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-nb-rNO/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-si/strings.xml | 1 + app/src/main/res/values-ta/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-zgh/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/arrays.xml | 12 +++++++++ app/src/main/res/values/strings.xml | 6 ++++- app/src/main/res/xml/root_preferences.xml | 10 +++++++ 33 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/io/github/subhamtyagi/ocr/utils/LanguageUtil.java create mode 100644 app/src/main/res/values-cn/arrays.xml diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java index f2e68574..1a147667 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java @@ -55,6 +55,7 @@ import io.github.subhamtyagi.ocr.spinner.SpinnerDialog; import io.github.subhamtyagi.ocr.utils.Constants; import io.github.subhamtyagi.ocr.utils.CrashUtils; +import io.github.subhamtyagi.ocr.utils.LanguageUtil; import io.github.subhamtyagi.ocr.utils.SpUtil; import io.github.subhamtyagi.ocr.utils.Utils; @@ -74,6 +75,7 @@ public class MainActivity extends AppCompatActivity implements TessBaseAPI.Progr * A spinner dialog shown on share menu */ private SpinnerDialog spinnerDialog; + private ArrayList countryCodes; private ArrayList languagesNames; private ArrayList languagesCodes; private CrashUtils crashUtils; @@ -124,9 +126,22 @@ public class MainActivity extends AppCompatActivity implements TessBaseAPI.Progr */ private TextView mLanguageName; + private void setLanguage() { + SpUtil.getInstance().init(this); + languagesCodes = new ArrayList<>(Arrays.asList(getResources().getStringArray(R.array.key_language_entries_value))); + countryCodes = new ArrayList<>(Arrays.asList(getResources().getStringArray(R.array.key_country_entries_value))); + String currentLanguage = SpUtil.getInstance().getString(Constants.CURRENT_LANGUAGE, ""); + if (getApplicationContext() == null || currentLanguage.isEmpty()) { + return; + } + String currentCountry = getCountryCodeFromLanguage(currentLanguage); + LanguageUtil.changeLanguage(MainActivity.this, currentLanguage, currentCountry); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setLanguage(); setContentView(R.layout.activity_main); /* @@ -248,6 +263,10 @@ private String getLanguageNameFromCode(String code, boolean multipleLanguages) { return multipleLanguages ? code : languagesNames.get(languagesCodes.indexOf(code)); } + private String getCountryCodeFromLanguage(String mLanguage){ + return countryCodes.isEmpty() ? "US" : countryCodes.get(languagesCodes.indexOf(mLanguage)); + } + @Override protected void onResume() { super.onResume(); diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java index 2e341044..dacf1222 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java @@ -1,6 +1,8 @@ package io.github.subhamtyagi.ocr; +import android.content.Context; import android.os.Bundle; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.ListPreference; @@ -37,8 +39,13 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { SwitchPreference enableMultipleLang = findPreference(getString(R.string.key_enable_multiple_lang)); ListPreference listPreference = findPreference(getString(R.string.key_language_for_tesseract)); + ListPreference listPreferenceLanguage = findPreference(getString(R.string.key_language)); + String oldValue = listPreferenceLanguage.getValue(); + listPreferenceLanguage.setOnPreferenceChangeListener((preference, newValue) -> { + if(!oldValue.equals(newValue)) showRestartAppDialog(requireContext()); + return true; + }); MultiSelectListPreference multiSelectListPreference = findPreference(getString(R.string.key_language_for_tesseract_multi)); - if (enableMultipleLang.isChecked()) { multiSelectListPreference.setVisible(true); listPreference.setVisible(false); @@ -58,6 +65,11 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } + + } + + private static void showRestartAppDialog(Context context){ + Toast.makeText(context, context.getString(R.string.the_app_needs_to_be_restarted), Toast.LENGTH_SHORT).show(); } public static class AdvanceSettingsFragment extends PreferenceFragmentCompat { @@ -68,4 +80,6 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } + + } \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java index f06506e1..1fefb3b9 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java @@ -33,7 +33,6 @@ public class Constants { public static final String KEY_OTSU_THRESHOLD = "otsu_threshold"; public static final String KEY_FIND_SKEW_AND_DESKEW = "deskew_img"; public static final String KEY_PAGE_SEG_MODE = "key_ocr_psm_mode"; - public static final String KEY_ENGINE_MODE = "key_oem_mode"; - - + public static final String KEY_ENGINE_MODE = "key_ocr_oem_mode"; + public static final String CURRENT_LANGUAGE = "key_language"; } diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/utils/LanguageUtil.java b/app/src/main/java/io/github/subhamtyagi/ocr/utils/LanguageUtil.java new file mode 100644 index 00000000..51b4461f --- /dev/null +++ b/app/src/main/java/io/github/subhamtyagi/ocr/utils/LanguageUtil.java @@ -0,0 +1,27 @@ +package io.github.subhamtyagi.ocr.utils; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.text.TextUtils; + +import java.util.Locale; + + +public class LanguageUtil { + + public static final void changeLanguage(Context context, String language, String country) { + Resources resources = context.getResources(); + Configuration config = resources.getConfiguration(); + config.locale = new Locale(language, country); + resources.updateConfiguration(config, null); + } + + public static final void followSystemLanguage(Context context) { + Resources resources = context.getResources(); + Configuration config = resources.getConfiguration(); + Locale locale = Locale.getDefault(); + config.locale = locale; + resources.updateConfiguration(config, null); + } + +} \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e1d5508e..e6860e5f 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -59,4 +59,5 @@ اسم اللغة حدد أو ابحث عن اللغات يحدد الدقة والكفاءة + localization \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index d9d6b0a8..16e2e673 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -59,4 +59,5 @@ Дадатковыя налады Паляпшэнне выявы Вызначае дакладнасць і эфектыўнасць + localization \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 506ee54e..394b5a00 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -15,4 +15,5 @@ টেসাররক্ট তথ্য অন্য পছন্দসমূহ Determines accuracy and efficiency + localization \ No newline at end of file diff --git a/app/src/main/res/values-cn/arrays.xml b/app/src/main/res/values-cn/arrays.xml new file mode 100644 index 00000000..4dc45150 --- /dev/null +++ b/app/src/main/res/values-cn/arrays.xml @@ -0,0 +1,7 @@ + + + + 简体中文 + 英语 + + \ No newline at end of file diff --git a/app/src/main/res/values-cn/strings.xml b/app/src/main/res/values-cn/strings.xml index a4d09744..bdaec1de 100644 --- a/app/src/main/res/values-cn/strings.xml +++ b/app/src/main/res/values-cn/strings.xml @@ -66,4 +66,7 @@ 存储文字识别结果的可选变量名 文字识别结果变量名 图片路径名 + 引擎模式 + 界面语言 + 需要重启应用才能生效 \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e9e95a4f..f8d67380 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -59,4 +59,5 @@ Název jazyka Vybrat nebo hledat jazyky Určuje přesnost a efektivitu + localization \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 51827d0f..b9a8a0be 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -59,4 +59,5 @@ Welche Sprache hat dieses Bild\? Name der Sprache Bestimmt Genauigkeit und Effizienz + localization \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d4f2efbb..0ac8074c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -59,4 +59,5 @@ Funciones para el tratamiento de las imágenes Dirige cómo Tesseract divide la imagen en líneas de texto y palabras. Determina la precisión y la eficiencia. + localization \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 4df16cde..dc415e72 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -59,4 +59,5 @@ انحراف را پیدا کرده و تصویر را اصلاح نمایید انجام باینری‌سازی آستانه Otsu تطبیقی محلی تصاویر دقت و کارایی را تعیین می کند + localization \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c73ec406..48ea19d2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -59,4 +59,5 @@ Nom de la langue Sélectionner ou rechercher des langues Détermine la précision et l’efficacité + localization \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index ef4d576e..b8ce2bb8 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -59,4 +59,5 @@ שיפור תמונה עיבוד טרום לתמונה לדיוק משופר קובע דיוק ויעילות + localization \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 9fa567cc..7ce88433 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -31,4 +31,5 @@ ऐप छवि पर पाठ खींचेगा छवि पर पाठ ड्रा करें सटीकता और दक्षता निर्धारित करता है + localization \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index b7b73f7c..9e83c3b7 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -59,4 +59,5 @@ Nama bahasa Pilih atau cari bahasa Menentukan akurasi dan efisiensi + localization \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f051c383..675e3759 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -59,4 +59,5 @@ Funzioni di elaborazione dell\'immagine Impostazioni Avanzate Tesseract Determina l\'accuratezza e l\'efficienza + localization \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 480d9c0a..bb1c8eaf 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -59,4 +59,5 @@ 이미지 향상 이미지 처리 기능 정확성과 효율성을 결정합니다. + localization \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 740c2c5e..5b903732 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -55,4 +55,5 @@ Avanserte Tesseract-innstillinger Bildebehandlingsfunksjoner Bestemmer nøyaktighet og effektivitet + localization \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2954cbbf..a4fcec7c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -38,4 +38,5 @@ Afbeelding Probeert adaptief het contrast uit te breiden naar het volledige dynamische bereik Bepaalt nauwkeurigheid en efficiëntie + localization \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4e9075e9..110629e9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -60,4 +60,5 @@ Nazwa języka Wybierz lub wyszukaj języki Określa dokładność i wydajność + localization \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8f77b648..62b6b1c2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -59,4 +59,5 @@ Modo de segmentação de página Não há dados de treinamento! Determina precisão e eficiência + localization \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 4d1ddeea..e1d43379 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -59,4 +59,5 @@ Selecionar ou pesquisar idiomas Idioma selecionado: Determina precisão e eficiência + localization \ No newline at end of file diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index b12adbb4..119dc5b6 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -20,4 +20,5 @@ බාගැනෙමින්… අවසාන ප්‍රතිඵලය පෙන්වන්න නිරවද්යතාව සහ කාර්යක්ෂමතාව තීරණය කරයි + localization \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 566fb518..6fec54d2 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,4 +1,5 @@ துல்லியம் மற்றும் செயல்திறனை தீர்மானிக்கிறது + localization \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 9c47c97b..5648beee 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -59,4 +59,5 @@ Bu resmin dili ne\? Dil Adı Doğruluğu ve verimliliği belirler + localization \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a2363671..ed527f5e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -59,4 +59,5 @@ Назва мови Вибір або пошук мов Визначає точність і ефективність + localization \ No newline at end of file diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index 592278d6..a93a485d 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -44,4 +44,5 @@ ⵃⴹⵓ ⵜⴰⵡⵍⴰⴼⵜ ⴷ ⵓⴹⵕⵉⵚ ⴷⴼⴼⵉⵔ ⵏ ⵡⵓⴼⵓⵖ ⵙⴳ ⵜⵙⵏⵙⵉ ⵓⵔ ⵍⵍⵉⵏⵜ ⵜⵎⵓⵛⴰ ⵏ ⵓⵙⴰⵏⵓⵏ! Determines accuracy and efficiency + localization \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f4e5621a..5c338778 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -59,4 +59,5 @@ 語言名稱 選擇/搜尋語言 決定準確性和效率 + localization \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 52aa451a..eae841cf 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -324,5 +324,17 @@ 2 3 + + Chinese-Simplified + English + + + cn + en + + + CN + US + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2806663c..48f8fb35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,7 +78,11 @@ Optional variable name that stores image text OCR text result Determines accuracy and efficiency - OCR Engine mode + OCR Engine mode + key_ocr_oem_mode The absolute path of the image in the application storage space The key-value information for the coordinates of each individual character + localization + key_language + You need to restart the application for it to take effect \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index cb4645a2..d93aa277 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -19,6 +19,16 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + Date: Sat, 9 Dec 2023 20:57:53 +0800 Subject: [PATCH 4/7] fix: The coordinates data format is optimized --- .../io/github/subhamtyagi/ocr/tasker/OCRConfig.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt index e09809f8..4ffb286e 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt +++ b/app/src/main/java/io/github/subhamtyagi/ocr/tasker/OCRConfig.kt @@ -67,8 +67,8 @@ class BackgroundWork: CoroutineScope by MainScope() { * to the bottom-right of the bottom-right contained pixel * @return A map. Key for each individual symbol, value for an array of coordinates */ - private fun readCoordinates(api: TessBaseAPI): Map> { - val coordinatesMap: MutableMap> = mutableMapOf() + private fun readCoordinates(api: TessBaseAPI): Map { + val coordinatesMap: MutableMap = mutableMapOf() val resultIterator = api.resultIterator resultIterator.begin() do { @@ -76,7 +76,7 @@ class BackgroundWork: CoroutineScope by MainScope() { val boundingBox = resultIterator.getBoundingBox(PageIteratorLevel.RIL_SYMBOL) if (symbol.isNotBlank()) { - coordinatesMap[symbol] = arrayOf(boundingBox) + coordinatesMap[symbol] = boundingBox } } while (resultIterator.next(PageIteratorLevel.RIL_SYMBOL)) @@ -87,13 +87,13 @@ class BackgroundWork: CoroutineScope by MainScope() { /** * Assemble the key-value information for the coordinates of each individual symbol. - * @return JSON string. eg:{"1":[[151,33,154,72]],"0":[[151,33,171,67]],"g":[[176,44,196,72]]} + * @return JSON string. eg:{"1":[151,33,154,72],"0":[151,33,171,67],"g":[176,44,196,72]} */ private fun assembleCoordinatesData(api: TessBaseAPI): String { val coordinatesMap = readCoordinates(api) val jsonObject = JSONObject() - coordinatesMap.forEach { (symbol, boundingBoxes) -> - jsonObject.put(symbol, JSONArray(boundingBoxes.map { JSONArray(it) })) + coordinatesMap.forEach { (symbol, boundingBox) -> + jsonObject.put(symbol, JSONArray().apply { boundingBox.forEach { put(it) } }) } Log.d("json data","===>$jsonObject") return jsonObject.toString() From 57f53a94a2981faa67b2130b52f4d07101550b77 Mon Sep 17 00:00:00 2001 From: Sauvio Date: Mon, 18 Dec 2023 14:04:51 +0800 Subject: [PATCH 5/7] fix: Save Last file into Android/data --- .../github/subhamtyagi/ocr/MainActivity.java | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java index 1a147667..7fc6665a 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java @@ -2,7 +2,6 @@ import android.annotation.SuppressLint; import android.app.ProgressDialog; -import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -12,7 +11,6 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.text.Html; @@ -549,15 +547,9 @@ public void saveBitmapToStorage(Bitmap bitmap) { File dir; File file; try { -// openFileOutput(..) will open file: /data/user/0/${packageName}/files/last_file.jpeg -// App data in "/storage/emulated/0/Android/data" on Android R - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - dir = CropFileProvider.filesDir(getApplicationContext()); - file = new File(dir, "last_file.jpeg"); - fileOutputStream = new FileOutputStream(file); - } else { - fileOutputStream = openFileOutput("last_file.jpeg", Context.MODE_PRIVATE); - } + dir = CropFileProvider.filesDir(getApplicationContext()); + file = new File(dir, "last_file.jpeg"); + fileOutputStream = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.JPEG, 30, fileOutputStream); fileOutputStream.close(); } catch (FileNotFoundException e) { @@ -575,18 +567,11 @@ public Bitmap loadBitmapFromStorage() { File dir; File file; try { -// openFileInput(..) will open file: /data/user/0/${packageName}/files/last_file.jpeg -// App data in "/storage/emulated/0/Android/data" on Android R - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - dir = CropFileProvider.filesDir(getApplicationContext()); - file = new File(dir, "last_file.jpeg"); - fileInputStream = new FileInputStream(file); - } else { - fileInputStream = openFileInput("last_file.jpeg");; - } + dir = CropFileProvider.filesDir(getApplicationContext()); + file = new File(dir, "last_file.jpeg"); + fileInputStream = new FileInputStream(file); bitmap = BitmapFactory.decodeStream(fileInputStream); fileInputStream.close(); - } catch (FileNotFoundException e) { e.printStackTrace(); crashUtils.logException(e); From db7a0018b4fa8909019ac6b02c783907a622cd19 Mon Sep 17 00:00:00 2001 From: Sauvio Date: Mon, 18 Dec 2023 15:35:12 +0800 Subject: [PATCH 6/7] feat: Dynamically update OCR engine mode options based on selected training data --- .../subhamtyagi/ocr/SettingsActivity.java | 23 ++++++++++++++++++- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-be/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-he/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-id/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-nb-rNO/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-si/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/arrays.xml | 8 +++++++ app/src/main/res/values/strings.xml | 4 ++-- app/src/main/res/xml/root_preferences.xml | 2 +- 28 files changed, 57 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java index dacf1222..9c4cf53b 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java @@ -62,9 +62,30 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { return true; }); - } + ListPreference tessTrainingDataPreference = findPreference(getString(R.string.key_tess_training_data_source)); + tessTrainingDataPreference.setOnPreferenceChangeListener((preference, newValue) -> { + String selectedTrainingData = (String) newValue; + updateOcrOemModeOptions(selectedTrainingData); + return true; + }); + } + /** + * tessdata-best and tessdata-fast DO NOT support legacy OCR engine mode + * @see Traineddata Files for Version 4.00 + + * @param selectedTrainingData + */ + private void updateOcrOemModeOptions(String selectedTrainingData) { + ListPreference ocrOemModePreference = findPreference(getString(R.string.key_engine_mode)); + if ("fast".equals(selectedTrainingData) || "best".equals(selectedTrainingData)) { + ocrOemModePreference.setEntries(R.array.key_lstm_ocr_oem_mode); + ocrOemModePreference.setEntryValues(R.array.value_lstm_ocr_oem_mode); + } else { + ocrOemModePreference.setEntries(R.array.key_ocr_oem_mode); + ocrOemModePreference.setEntryValues(R.array.value_ocr_oem_mode); + } + } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e6860e5f..66cad455 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -59,5 +59,5 @@ اسم اللغة حدد أو ابحث عن اللغات يحدد الدقة والكفاءة - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 16e2e673..4445b1cb 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -59,5 +59,5 @@ Дадатковыя налады Паляпшэнне выявы Вызначае дакладнасць і эфектыўнасць - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 394b5a00..aa8bdda4 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -15,5 +15,5 @@ টেসাররক্ট তথ্য অন্য পছন্দসমূহ Determines accuracy and efficiency - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f8d67380..f6193b19 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -59,5 +59,5 @@ Název jazyka Vybrat nebo hledat jazyky Určuje přesnost a efektivitu - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b9a8a0be..afa1f320 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -59,5 +59,5 @@ Welche Sprache hat dieses Bild\? Name der Sprache Bestimmt Genauigkeit und Effizienz - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0ac8074c..058ab0d8 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -59,5 +59,5 @@ Funciones para el tratamiento de las imágenes Dirige cómo Tesseract divide la imagen en líneas de texto y palabras. Determina la precisión y la eficiencia. - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index dc415e72..e8f8ddbe 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -59,5 +59,5 @@ انحراف را پیدا کرده و تصویر را اصلاح نمایید انجام باینری‌سازی آستانه Otsu تطبیقی محلی تصاویر دقت و کارایی را تعیین می کند - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 48ea19d2..c1f05487 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -59,5 +59,5 @@ Nom de la langue Sélectionner ou rechercher des langues Détermine la précision et l’efficacité - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index b8ce2bb8..c59f441d 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -59,5 +59,5 @@ שיפור תמונה עיבוד טרום לתמונה לדיוק משופר קובע דיוק ויעילות - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 7ce88433..86f774d7 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -31,5 +31,5 @@ ऐप छवि पर पाठ खींचेगा छवि पर पाठ ड्रा करें सटीकता और दक्षता निर्धारित करता है - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 9e83c3b7..dc0b52be 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -59,5 +59,5 @@ Nama bahasa Pilih atau cari bahasa Menentukan akurasi dan efisiensi - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 675e3759..991475c4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -59,5 +59,5 @@ Funzioni di elaborazione dell\'immagine Impostazioni Avanzate Tesseract Determina l\'accuratezza e l\'efficienza - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bb1c8eaf..08bca55d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -59,5 +59,5 @@ 이미지 향상 이미지 처리 기능 정확성과 효율성을 결정합니다. - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 5b903732..1705eb8d 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -55,5 +55,5 @@ Avanserte Tesseract-innstillinger Bildebehandlingsfunksjoner Bestemmer nøyaktighet og effektivitet - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a4fcec7c..0897c20b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -38,5 +38,5 @@ Afbeelding Probeert adaptief het contrast uit te breiden naar het volledige dynamische bereik Bepaalt nauwkeurigheid en efficiëntie - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 110629e9..d8519c85 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -60,5 +60,5 @@ Nazwa języka Wybierz lub wyszukaj języki Określa dokładność i wydajność - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 62b6b1c2..35550e99 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -59,5 +59,5 @@ Modo de segmentação de página Não há dados de treinamento! Determina precisão e eficiência - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index e1d43379..56095629 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -59,5 +59,5 @@ Selecionar ou pesquisar idiomas Idioma selecionado: Determina precisão e eficiência - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 119dc5b6..ecccea44 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -20,5 +20,5 @@ බාගැනෙමින්… අවසාන ප්‍රතිඵලය පෙන්වන්න නිරවද්යතාව සහ කාර්යක්ෂමතාව තීරණය කරයි - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 6fec54d2..a299263b 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,5 +1,5 @@ துல்லியம் மற்றும் செயல்திறனை தீர்மானிக்கிறது - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5648beee..f5a57fdf 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -59,5 +59,5 @@ Bu resmin dili ne\? Dil Adı Doğruluğu ve verimliliği belirler - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ed527f5e..e2bcdd20 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -59,5 +59,5 @@ Назва мови Вибір або пошук мов Визначає точність і ефективність - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index a93a485d..902be922 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -44,5 +44,5 @@ ⵃⴹⵓ ⵜⴰⵡⵍⴰⴼⵜ ⴷ ⵓⴹⵕⵉⵚ ⴷⴼⴼⵉⵔ ⵏ ⵡⵓⴼⵓⵖ ⵙⴳ ⵜⵙⵏⵙⵉ ⵓⵔ ⵍⵍⵉⵏⵜ ⵜⵎⵓⵛⴰ ⵏ ⵓⵙⴰⵏⵓⵏ! Determines accuracy and efficiency - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 5c338778..e3d07832 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -59,5 +59,5 @@ 語言名稱 選擇/搜尋語言 決定準確性和效率 - localization + Localization \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index eae841cf..572cf22a 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -318,12 +318,20 @@ OEM_TESSERACT_LSTM_COMBINED OEM_DEFAULT + + OEM_LSTM_ONLY + OEM_DEFAULT + 0 1 2 3 + + 1 + 3 + Chinese-Simplified English diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48f8fb35..eecc1243 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,11 +78,11 @@ Optional variable name that stores image text OCR text result Determines accuracy and efficiency - OCR Engine mode + OCR engine mode key_ocr_oem_mode The absolute path of the image in the application storage space The key-value information for the coordinates of each individual character - localization + Localization key_language You need to restart the application for it to take effect \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index d93aa277..47d98392 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -81,7 +81,7 @@ android:defaultValue="3" android:entries="@array/key_ocr_oem_mode" android:entryValues="@array/value_ocr_oem_mode" - android:key="key_ocr_oem_mode" + android:key="@string/key_engine_mode" android:summary="@string/oem_summary" android:title="@string/engine_mode" app:icon="@drawable/ic_baseline_engine_mode_32"> From 99c1e4eda1cebe194c3cab8f62efc0daf5ae76ab Mon Sep 17 00:00:00 2001 From: Sauvio Date: Tue, 19 Dec 2023 12:21:16 +0800 Subject: [PATCH 7/7] fix:1. Engine mode and Tesseract Data type are now more user-friendly for user settings. 2. The download link for the traineddata files has been updated to Version 4.00+. --- .../github/subhamtyagi/ocr/MainActivity.java | 3 +- .../subhamtyagi/ocr/SettingsActivity.java | 141 ++++++++++++------ .../subhamtyagi/ocr/ocr/ImageTextReader.java | 4 +- .../subhamtyagi/ocr/utils/Constants.java | 6 +- app/src/main/res/values/arrays.xml | 8 + 5 files changed, 111 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java index 7fc6665a..e5c40482 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/MainActivity.java @@ -337,7 +337,8 @@ public void run() { if (mImageTextReader != null) { mImageTextReader.tearDownEverything(); } - mImageTextReader = ImageTextReader.geInstance(cf.getAbsolutePath(), mLanguage, mPageSegMode, MainActivity.this::onProgressValues); + int mEngineMode = Utils.getEngineMode(); + mImageTextReader = ImageTextReader.geInstance(cf.getAbsolutePath(), mLanguage, mPageSegMode, mEngineMode, MainActivity.this::onProgressValues); //check if current language data is valid //if it is invalid(i.e. corrupted, half downloaded, tempered) then delete it if (!mImageTextReader.success) { diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java b/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java index 9c4cf53b..67dc72cd 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/SettingsActivity.java @@ -1,13 +1,17 @@ package io.github.subhamtyagi.ocr; import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.ListPreference; import androidx.preference.MultiSelectListPreference; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreference; @@ -32,65 +36,66 @@ protected void onCreate(Bundle savedInstanceState) { }*/ } - public static class SettingsFragment extends PreferenceFragmentCompat { + public static class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener{ @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); SwitchPreference enableMultipleLang = findPreference(getString(R.string.key_enable_multiple_lang)); ListPreference listPreference = findPreference(getString(R.string.key_language_for_tesseract)); - ListPreference listPreferenceLanguage = findPreference(getString(R.string.key_language)); - String oldValue = listPreferenceLanguage.getValue(); - listPreferenceLanguage.setOnPreferenceChangeListener((preference, newValue) -> { - if(!oldValue.equals(newValue)) showRestartAppDialog(requireContext()); - return true; - }); MultiSelectListPreference multiSelectListPreference = findPreference(getString(R.string.key_language_for_tesseract_multi)); - if (enableMultipleLang.isChecked()) { - multiSelectListPreference.setVisible(true); - listPreference.setVisible(false); - } else { - multiSelectListPreference.setVisible(false); - listPreference.setVisible(true); - } - - enableMultipleLang.setOnPreferenceChangeListener((preference, newValue) -> { - boolean b = (boolean) newValue; - multiSelectListPreference.setVisible(b); - listPreference.setVisible(!b); - - return true; - }); - - ListPreference tessTrainingDataPreference = findPreference(getString(R.string.key_tess_training_data_source)); - tessTrainingDataPreference.setOnPreferenceChangeListener((preference, newValue) -> { - String selectedTrainingData = (String) newValue; - updateOcrOemModeOptions(selectedTrainingData); - return true; - }); + handleMultiLanguageChange(enableMultipleLang.isChecked(), listPreference, multiSelectListPreference); + ListPreference ocrOemModePreference = findPreference(getString(R.string.key_engine_mode)); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); + String tessTrainingDataSourceValue = sharedPreferences.getString(getString(R.string.key_tess_training_data_source), ""); + handlePreferenceTessTrainingDataSourceChange(tessTrainingDataSourceValue, ocrOemModePreference); + ListPreference tessTrainingDataSourcePreference = findPreference(getString(R.string.key_tess_training_data_source)); + String keyEngineModeValue = sharedPreferences.getString(getString(R.string.key_engine_mode), ""); + handlePreferenceOcrOemModeChange(keyEngineModeValue, tessTrainingDataSourcePreference); + + findPreference(getString(R.string.key_language)).setOnPreferenceChangeListener(this); + findPreference(getString(R.string.key_enable_multiple_lang)).setOnPreferenceChangeListener(this); + findPreference(getString(R.string.key_tess_training_data_source)).setOnPreferenceChangeListener(this); + findPreference(getString(R.string.key_engine_mode)).setOnPreferenceChangeListener(this); + + findPreference(getString(R.string.key_enable_multiple_lang)).setOnPreferenceClickListener(this); } - /** - * tessdata-best and tessdata-fast DO NOT support legacy OCR engine mode - * @see Traineddata Files for Version 4.00 + - * @param selectedTrainingData - */ - private void updateOcrOemModeOptions(String selectedTrainingData) { - ListPreference ocrOemModePreference = findPreference(getString(R.string.key_engine_mode)); - if ("fast".equals(selectedTrainingData) || "best".equals(selectedTrainingData)) { - ocrOemModePreference.setEntries(R.array.key_lstm_ocr_oem_mode); - ocrOemModePreference.setEntryValues(R.array.value_lstm_ocr_oem_mode); - } else { - ocrOemModePreference.setEntries(R.array.key_ocr_oem_mode); - ocrOemModePreference.setEntryValues(R.array.value_ocr_oem_mode); + @Override + public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) { + + if (preference.getKey().equals(getString(R.string.key_language))) { + Context context = requireContext(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); + String previousValue = sharedPreferences.getString(getString(R.string.key_language_for_tesseract),""); + handlePreferenceLanguageChange(context, previousValue, newValue.toString()); + } else if(preference.getKey().equals(getString(R.string.key_enable_multiple_lang))){ + ListPreference listPreference = findPreference(getString(R.string.key_language_for_tesseract)); + MultiSelectListPreference multiSelectListPreference = findPreference(getString(R.string.key_language_for_tesseract_multi)); + handleMultiLanguageChange((boolean)newValue, listPreference, multiSelectListPreference); + } else if(preference.getKey().equals(getString(R.string.key_tess_training_data_source))){ + ListPreference ocrOemModePreference = findPreference(getString(R.string.key_engine_mode)); + handlePreferenceTessTrainingDataSourceChange((String) newValue, ocrOemModePreference); + } else if(preference.getKey().equals(getString(R.string.key_engine_mode))){ + ListPreference tessTrainingDataSourcePreference = findPreference(getString(R.string.key_tess_training_data_source)); + handlePreferenceOcrOemModeChange((String)newValue, tessTrainingDataSourcePreference); } + + return true; } - } - private static void showRestartAppDialog(Context context){ - Toast.makeText(context, context.getString(R.string.the_app_needs_to_be_restarted), Toast.LENGTH_SHORT).show(); + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + if(preference.getKey().equals(getString(R.string.key_enable_multiple_lang))){ + SwitchPreference enableMultipleLang = (SwitchPreference) preference; + ListPreference listPreference = findPreference(getString(R.string.key_language_for_tesseract)); + MultiSelectListPreference multiSelectListPreference = findPreference(getString(R.string.key_language_for_tesseract_multi)); + handleMultiLanguageClick(enableMultipleLang.isChecked(), listPreference, multiSelectListPreference); + } + return true; + } } public static class AdvanceSettingsFragment extends PreferenceFragmentCompat { @@ -102,5 +107,51 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } + private static void handlePreferenceLanguageChange(Context context,String previousValue, String newValue){ + if(!previousValue.equals(newValue)) + Toast.makeText(context, context.getString(R.string.the_app_needs_to_be_restarted), Toast.LENGTH_SHORT).show(); + } + + /** + * When using the traineddata files from the tessdata_best and tessdata_fast repositories, + * only the new LSTM-based OCR engine (–oem 1) is supported + * @see Traineddata Files for Version 4.00 + + */ + private static void handlePreferenceTessTrainingDataSourceChange(String newValue, ListPreference ocrOemModePreference) { + if ("fast".equals(newValue) || "best".equals(newValue)) { + ocrOemModePreference.setEntries(R.array.key_lstm_ocr_oem_mode); + ocrOemModePreference.setEntryValues(R.array.value_lstm_ocr_oem_mode); + } else { + ocrOemModePreference.setEntries(R.array.key_ocr_oem_mode); + ocrOemModePreference.setEntryValues(R.array.value_ocr_oem_mode); + } + } + + /** + * Tesseract’s oem modes ‘0’ and ‘2’ won’t work with tessdata_best and tessdata_fast repositories + * @see Traineddata Files for Version 4.00 + + */ + private static void handlePreferenceOcrOemModeChange(String newValue, ListPreference tessTrainingDataSourcePreference) { + if("0".equals(newValue) || "2".equals(newValue)){ + tessTrainingDataSourcePreference.setEntries(R.array.ocr_training_data_legacy_entries); + tessTrainingDataSourcePreference.setEntryValues(R.array.key_ocr_training_data_legacy_entries_values); + } else { + tessTrainingDataSourcePreference.setEntries(R.array.ocr_training_data_entries); + tessTrainingDataSourcePreference.setEntryValues(R.array.key_ocr_training_data_entries_values); + } + } + private static void handleMultiLanguageChange(boolean newValue, ListPreference listPreference, MultiSelectListPreference multiSelectListPreference){ + multiSelectListPreference.setVisible(newValue); + listPreference.setVisible(!newValue); + } + private static void handleMultiLanguageClick(boolean checked, ListPreference listPreference, MultiSelectListPreference multiSelectListPreference) { + if (checked) { + multiSelectListPreference.setVisible(true); + listPreference.setVisible(false); + } else { + multiSelectListPreference.setVisible(false); + listPreference.setVisible(true); + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/ocr/ImageTextReader.java b/app/src/main/java/io/github/subhamtyagi/ocr/ocr/ImageTextReader.java index 36550f4e..1b85c255 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/ocr/ImageTextReader.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/ocr/ImageTextReader.java @@ -24,11 +24,11 @@ public class ImageTextReader { * @param language language code i.e. selected by user * @return the instance of this class for later use */ - public static ImageTextReader geInstance(String path, String language,int pageSegMode, TessBaseAPI.ProgressNotifier progressNotifier) { + public static ImageTextReader geInstance(String path, String language,int pageSegMode, int engineMode, TessBaseAPI.ProgressNotifier progressNotifier) { try { ImageTextReader imageTextReader=new ImageTextReader(); api = new TessBaseAPI(progressNotifier); - imageTextReader.success = api.init(path, language); + imageTextReader.success = api.init(path, language, engineMode); api.setPageSegMode(pageSegMode); return imageTextReader; } catch (Exception e) { diff --git a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java index 1fefb3b9..4ee94879 100644 --- a/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java +++ b/app/src/main/java/io/github/subhamtyagi/ocr/utils/Constants.java @@ -9,9 +9,9 @@ public class Constants { /*** *TRAINING DATA URL TEMPLATES for downloading */ - public static final String TESSERACT_DATA_DOWNLOAD_URL_BEST = "https://github.com/tesseract-ocr/tessdata_best/raw/4.0.0/%s.traineddata"; - public static final String TESSERACT_DATA_DOWNLOAD_URL_STANDARD = "https://github.com/tesseract-ocr/tessdata/raw/4.0.0/%s.traineddata"; - public static final String TESSERACT_DATA_DOWNLOAD_URL_FAST = "https://github.com/tesseract-ocr/tessdata_fast/raw/4.0.0/%s.traineddata"; + public static final String TESSERACT_DATA_DOWNLOAD_URL_BEST = "https://github.com/tesseract-ocr/tessdata_best/raw/main/%s.traineddata"; + public static final String TESSERACT_DATA_DOWNLOAD_URL_STANDARD = "https://github.com/tesseract-ocr/tessdata/raw/main/%s.traineddata"; + public static final String TESSERACT_DATA_DOWNLOAD_URL_FAST = "https://github.com/tesseract-ocr/tessdata_fast/raw/main/%s.traineddata"; public static final String LANGUAGE_CODE = "%s.traineddata"; diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 572cf22a..f85366c2 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -273,12 +273,20 @@ Standard + + Standard + + fast best standard + + standard + + PSM_OSD_ONLY PSM_AUTO_OSD