Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.stypox.dicio.skills.search.SearchInfo
import org.stypox.dicio.skills.telephone.TelephoneInfo
import org.stypox.dicio.skills.timer.TimerInfo
import org.stypox.dicio.skills.weather.WeatherInfo
import org.stypox.dicio.skills.app_search.AppSearchInfo
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -42,6 +43,7 @@ class SkillHandler @Inject constructor(
val allSkillInfoList = listOf(
WeatherInfo,
SearchInfo,
AppSearchInfo,
LyricsInfo,
OpenInfo,
CalculatorInfo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.stypox.dicio.skills.app_search

import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import org.dicio.skill.context.SkillContext
import org.dicio.skill.skill.Skill
import org.dicio.skill.skill.SkillInfo
import org.stypox.dicio.R
import org.stypox.dicio.sentences.Sentences

object AppSearchInfo : SkillInfo("app_search") {
override fun name(context: Context) =
context.getString(R.string.skill_name_app_search)

override fun sentenceExample(context: Context) =
context.getString(R.string.skill_sentence_example_app_search)

@Composable
override fun icon() =
rememberVectorPainter(Icons.Filled.Search)

override fun isAvailable(ctx: SkillContext): Boolean {
return Sentences.AppSearch[ctx.sentencesLanguage] != null
}

override fun build(ctx: SkillContext): Skill<*> {
return AppSearchSkill(AppSearchInfo, Sentences.AppSearch[ctx.sentencesLanguage]!!)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.stypox.dicio.skills.app_search

import android.content.pm.PackageManager
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.dicio.skill.context.SkillContext
import org.dicio.skill.skill.SkillOutput
import org.stypox.dicio.R
import org.stypox.dicio.io.graphical.Headline
import org.stypox.dicio.util.getString

private val TAG = AppSearchOutput::class.simpleName

class AppSearchOutput(
private val appName: String?,
private val packageName: String?,
private val searchQuery: String?,
private val success: Boolean
) : SkillOutput {
Comment on lines +30 to +35
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having a class with nullable fields, you can create a sealed interface with one class for each possible outcome. It's not so necessary, but it might make things a bit more clear. See how it's done e.g. for the Lyrics, there is just one failure mode there while here you would have three. Also, to avoid having to implement GraphicalOutput multiple times, you can make the error classes override HeadlineSpeechSkillOutput.

override fun getSpeechOutput(ctx: SkillContext): String = when {
appName == null -> ctx.getString(R.string.skill_app_search_could_not_understand)
packageName == null -> ctx.getString(R.string.skill_app_search_unknown_app, appName)
!success -> ctx.getString(R.string.skill_app_search_failed, appName, searchQuery ?: "")
else -> ctx.getString(R.string.skill_app_search_searching, appName, searchQuery ?: "")
}

@Composable
override fun GraphicalOutput(ctx: SkillContext) {
if (appName == null || packageName == null || !success) {
Headline(text = getSpeechOutput(ctx))
} else {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
val context = LocalContext.current
val icon = remember {
try {
context.packageManager.getApplicationIcon(packageName)
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Could not load icon for $packageName", e)
null
}
}

if (icon != null) {
Image(
painter = rememberDrawablePainter(icon),
contentDescription = appName,
modifier = Modifier
.fillMaxWidth(0.2f)
.aspectRatio(1.0f),
)

Spacer(modifier = Modifier.width(8.dp))
}

Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = getSpeechOutput(ctx),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)

Text(
text = packageName,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.stypox.dicio.skills.app_search

import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import org.dicio.skill.context.SkillContext
import org.dicio.skill.skill.SkillInfo
import org.dicio.skill.skill.SkillOutput
import org.dicio.skill.standard.StandardRecognizerData
import org.dicio.skill.standard.StandardRecognizerSkill
import org.stypox.dicio.sentences.Sentences.AppSearch
import org.stypox.dicio.util.StringUtils

class AppSearchSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData<AppSearch>)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that we need another skill, maybe it's better to extend the search skill that already exists. The search skill however has a low specificity, while this one has a high specificity at the moment (although I would suggest a medium specificity would be better), so we need to see how it plays out, maybe it is a good idea to have two separate skills.

: StandardRecognizerSkill<AppSearch>(correspondingSkillInfo, data) {

override suspend fun generateOutput(ctx: SkillContext, inputData: AppSearch): SkillOutput {
val userAppName = when (inputData) {
is AppSearch.Query -> inputData.app?.trim { it <= ' ' }
else -> null
}
val userQuery = when (inputData) {
is AppSearch.Query -> inputData.query?.trim { it <= ' ' }
else -> null
}

val packageManager: PackageManager = ctx.android.packageManager
val applicationInfo = userAppName?.let { getMostSimilarApp(packageManager, it) }
var success = false

if (applicationInfo != null && userQuery != null) {
success = launchAppWithSearch(ctx, packageManager, applicationInfo, userQuery)
}

return AppSearchOutput(
appName = applicationInfo?.loadLabel(packageManager)?.toString() ?: userAppName,
packageName = applicationInfo?.packageName,
searchQuery = userQuery,
success = success
)
}

private fun launchAppWithSearch(ctx: SkillContext, packageManager: PackageManager, applicationInfo: ApplicationInfo, query: String): Boolean {
val packageName = applicationInfo.packageName
try {
// Handle different apps with their specific search intents
when {
// YouTube
packageName.contains("youtube", ignoreCase = true) -> {
val intent = Intent(Intent.ACTION_SEARCH)
intent.setPackage(packageName)
intent.putExtra("query", query)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
ctx.android.startActivity(intent)
return true
}
// Spotify
packageName.contains("spotify", ignoreCase = true) -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.setData(Uri.parse("spotify:search:$query"))
intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse("android-app://${ctx.android.packageName}"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
ctx.android.startActivity(intent)
return true
}
// Generic search intent as fallback
else -> {
val intent = Intent(Intent.ACTION_SEARCH)
intent.setPackage(packageName)
intent.putExtra("query", query)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
ctx.android.startActivity(intent)
return true
}
}
} catch (e: Exception) {
// If the specific intent fails, try a web search as fallback
try {
val intent = Intent(Intent.ACTION_VIEW)
when {
packageName.contains("youtube", ignoreCase = true) -> {
intent.data = Uri.parse("https://www.youtube.com/results?search_query=${Uri.encode(query)}")
}
packageName.contains("spotify", ignoreCase = true) -> {
intent.data = Uri.parse("https://open.spotify.com/search/${Uri.encode(query)}")
}
else -> {
return false
}
}
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
ctx.android.startActivity(intent)
return true
} catch (e: Exception) {
return false
}
}
}

companion object {
private fun getMostSimilarApp(
packageManager: PackageManager,
appName: String
): ApplicationInfo? {
val resolveInfosIntent = Intent(Intent.ACTION_MAIN, null)
resolveInfosIntent.addCategory(Intent.CATEGORY_LAUNCHER)

@SuppressLint("QueryPermissionsNeeded") // we need to query all apps
val resolveInfos: List<ResolveInfo> =
packageManager.queryIntentActivities(resolveInfosIntent, 0)
var bestDistance = Int.MAX_VALUE
var bestApplicationInfo: ApplicationInfo? = null

for (resolveInfo in resolveInfos) {
try {
val currentApplicationInfo: ApplicationInfo = packageManager.getApplicationInfo(
resolveInfo.activityInfo.packageName, PackageManager.GET_META_DATA
)
val currentDistance = StringUtils.customStringDistance(
appName,
packageManager.getApplicationLabel(currentApplicationInfo).toString()
)
if (currentDistance < bestDistance) {
bestDistance = currentDistance
bestApplicationInfo = currentApplicationInfo
}
} catch (ignored: PackageManager.NameNotFoundException) {
}
}
return if (bestDistance > 5) null else bestApplicationInfo
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<string name="skill_sentence_example_weather">¿Cuál es el clima en Roma\?</string>
<string name="skill_name_lyrics">Letras de canciones</string>
<string name="skill_sentence_example_lyrics">¿Cuál es la canción que dice we will we will rock you\?</string>
<string name="skill_name_app_search">Búsqueda en Aplicaciones</string>
<string name="skill_sentence_example_app_search">En YouTube buscar gatos lindos</string>
<string name="skill_name_open">Abrir aplicaciones</string>
<string name="skill_sentence_example_open">Abrir newpipe</string>
<string name="skill_sentence_example_calculator">¿Cuánto es cinco por cuatro menos un millón\?</string>
Expand All @@ -61,6 +63,10 @@
<string name="skill_lyrics_found_song_by_artist">Se ha encontrado la canción %1$s de %2$s</string>
<string name="skill_open_unknown_app">Aplicación desconocida %1$s</string>
<string name="skill_open_opening">Abriendo %1$s…</string>
<string name="skill_app_search_could_not_understand">No pude entender la aplicación</string>
<string name="skill_app_search_unknown_app">Aplicación desconocida %1$s</string>
<string name="skill_app_search_searching">Buscando %2$s en %1$s</string>
<string name="skill_app_search_failed">No se pudo buscar %2$s en %1$s</string>
<string name="skill_calculator_could_not_calculate">No pude calcular tu solicitud</string>
<string name="eval_network_error">Error de la red</string>
<string name="drawer_close">Cerrar la caja de navegación</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
<string name="skill_name_timer">Timer</string>
<string name="skill_sentence_example_timer">Set a timer for five minutes</string>
<string name="skill_name_current_time">Current time</string>
<string name="skill_name_app_search">App Search</string>
<string name="skill_sentence_example_app_search">In YouTube search for cute cats</string>
Comment on lines +152 to +153
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<string name="skill_name_app_search">App Search</string>
<string name="skill_sentence_example_app_search">In YouTube search for cute cats</string>
<string name="skill_name_app_search">Search in apps</string>
<string name="skill_sentence_example_app_search">Search for cute cats on YouTube</string>

<string name="skill_sentence_example_current_time">What time is it?</string>
<string name="skill_fallback_name_text">Text message</string>
<string name="skill_search_here_is_what_i_found">Here is what I found</string>
Expand All @@ -164,6 +166,10 @@
<string name="skill_open_could_not_understand">Could not understand app</string>
<string name="skill_open_unknown_app">Unknown app %1$s</string>
<string name="skill_open_opening">Opening %1$s…</string>
<string name="skill_app_search_could_not_understand">Could not understand app</string>
<string name="skill_app_search_unknown_app">Unknown app %1$s</string>
<string name="skill_app_search_searching">Searching for %2$s on %1$s</string>
<string name="skill_app_search_failed">Failed to search for %2$s on %1$s</string>
<string name="skill_media_playing">Playing media…</string>
<string name="skill_media_pausing">Pausing media…</string>
<string name="skill_media_previous">Previous media…</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/sentences/en/app_search.yml
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could condense these into "in|on .app. (search for?)|lookup|(look up|for)|find .query.". You could also add another sentence with "in|on .app." at the end, the sentence recognizer should still be able to distinguish it from searching in general: "(search for?)|lookup|(look up|for)|find .query. in|on .app.".

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query:
- in .app. look up .query.
- in .app. find .query.
- in .app. search .query.
- in .app. search for .query.

7 changes: 7 additions & 0 deletions app/src/main/sentences/es/app_search.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query:
- en .app. buscar .query.
- en .app. busca .query.
- en .app. busque .query.
- en .app. encontrar .query.
- en .app. encuentra .query.
- en .app. encuentre .query.
10 changes: 10 additions & 0 deletions app/src/main/sentences/skill_definitions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
skills:
- id: app_search
specificity: high
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
specificity: high
specificity: medium

sentences:
- id: query
captures:
- id: app
type: string
- id: query
type: string

- id: current_time
specificity: high
sentences:
Expand Down