book-reader/app/src/main/java/com/aryan/reader/epubreader/ExternalDictionaryHelper.kt
Aryan 76780bff31
Offline dictionary (#45)
* Added support for external dictionary apps and a dictionary settings sheet.

* Refactored external dictionary integration and improved package visibility

 - Updated `AndroidManifest.xml` with `<queries>` for common dictionary apps and intent actions (`PROCESS_TEXT`, `SEARCH`, `SEND`) to support Android 11+ package visibility.
 - Refactored `ExternalDictionaryHelper` to streamline dictionary discovery using `Intent.ACTION_PROCESS_TEXT` and `colordict.intent.action.SEARCH`.
 - Added a package blocklist in `ExternalDictionaryHelper` to filter out system services (e.g., Samsung Pass, GMS) from dictionary lists.
  - Simplified `launchDictionary` logic to prioritize `PROCESS_TEXT` intents.
 - Fixed a bug in `PdfViewerScreen` to only show the dictionary upsell dialog when `useOnlineDictionary` is enabled.

* Refactored dictionary settings into a dialog and updated dictionary selection logic.

- Renamed `DictionarySettingsSheet` to `DictionarySettingsDialog` and converted it from a bottom sheet to a formal dialog.
- Redesigned the dictionary settings UI with a clearer distinction between AI and external app modes.
- Added a "Search" option to the external dictionary list that triggers a web search.
- Updated `ExternalDictionaryHelper` to include a package blocklist and support for generic web search intents.
- Simplified `PdfHelper` by removing Pro user checks for the "Dictionary" action.
- Updated icons for dictionary settings across the EPUB and PDF readers.
2026-03-08 15:19:52 +05:30

154 lines
No EOL
5.3 KiB
Kotlin

// ExternalDictionaryHelper.kt
package com.aryan.reader.epubreader
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Build
import android.widget.Toast
import timber.log.Timber
data class ExternalDictionaryApp(
val label: String,
val packageName: String,
val icon: Drawable?
)
object ExternalDictionaryHelper {
const val GOOGLE_SEARCH_PKG = "app.internal.google_search"
private val PACKAGE_BLOCKLIST = setOf(
"com.samsung.android.samsungpassautofill",
"com.samsung.android.samsungpass",
"com.samsung.android.app.pass",
"com.google.android.gms",
"com.truecaller",
"com.adobe.reader",
"com.reddit.frontpage"
)
fun getAvailableDictionaries(context: Context): List<ExternalDictionaryApp> {
val pm = context.packageManager
val apps = mutableListOf<ExternalDictionaryApp>()
val addedPackages = mutableSetOf<String>()
val processTextIntent = Intent(Intent.ACTION_PROCESS_TEXT).setType("text/plain")
val textResolvers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.queryIntentActivities(processTextIntent, PackageManager.ResolveInfoFlags.of(0))
} else {
@Suppress("DEPRECATION") pm.queryIntentActivities(processTextIntent, 0)
}
textResolvers.forEach { ri ->
val pkg = ri.activityInfo.packageName
if (!PACKAGE_BLOCKLIST.contains(pkg) && addedPackages.add(pkg)) {
apps.add(ExternalDictionaryApp(
label = ri.loadLabel(pm).toString(),
packageName = pkg,
icon = ri.loadIcon(pm)
))
}
}
val colorDictIntent = Intent("colordict.intent.action.SEARCH")
val colorResolvers = pm.queryIntentActivities(colorDictIntent, 0)
colorResolvers.forEach { ri ->
val pkg = ri.activityInfo.packageName
if (!PACKAGE_BLOCKLIST.contains(pkg) && addedPackages.add(pkg)) {
apps.add(ExternalDictionaryApp(
label = ri.loadLabel(pm).toString(),
packageName = pkg,
icon = ri.loadIcon(pm)
))
}
}
val sortedApps = apps.sortedBy { it.label }.toMutableList()
// Inject Google Search at the top
sortedApps.add(
0,
ExternalDictionaryApp(
label = "Search",
packageName = GOOGLE_SEARCH_PKG,
icon = null
)
)
return sortedApps
}
fun launchDictionary(context: Context, packageName: String, query: String) {
val pm = context.packageManager
try {
if (packageName == GOOGLE_SEARCH_PKG) {
val searchIntent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(searchIntent)
return
}
val processTextIntent = Intent(Intent.ACTION_PROCESS_TEXT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_PROCESS_TEXT, query)
putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
setPackage(packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (processTextIntent.resolveActivity(pm) != null) {
context.startActivity(processTextIntent)
return
}
val dictIntent = Intent("colordict.intent.action.SEARCH").apply {
putExtra("EXTRA_QUERY", query)
setPackage(packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (dictIntent.resolveActivity(pm) != null) {
context.startActivity(dictIntent)
return
}
if (packageName == "it.t_arn.aard2") {
val aardIntent = Intent("aard2.lookup").apply {
putExtra("query", query)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(aardIntent)
return
}
launchGenericSend(context, packageName, query)
} catch (e: Exception) {
Timber.e(e, "Failed to launch dictionary app: $packageName")
Toast.makeText(context, "Error opening dictionary", Toast.LENGTH_SHORT).show()
}
}
private fun launchGenericSend(context: Context, packageName: String, query: String) {
val sendIntent = Intent(Intent.ACTION_SEND)
sendIntent.type = "text/plain"
sendIntent.putExtra(Intent.EXTRA_TEXT, query)
sendIntent.setPackage(packageName)
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(sendIntent)
} catch (e: Exception) {
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
if (launchIntent != null) {
context.startActivity(launchIntent)
} else {
throw e
}
}
}
}