From 71546b7fb78612591baa38140eb3a70ee9a3bce5 Mon Sep 17 00:00:00 2001 From: Gabriel <69007475+Friendly-Banana@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:29:32 +0100 Subject: [PATCH] add music skill for playing songs --- .../org/stypox/dicio/eval/SkillHandler.kt | 2 + .../stypox/dicio/skills/music/MusicInfo.kt | 39 ++++++++ .../stypox/dicio/skills/music/MusicOutput.kt | 92 +++++++++++++++++++ .../stypox/dicio/skills/music/MusicSkill.kt | 48 ++++++++++ app/src/main/res/values/strings.xml | 3 + app/src/main/sentences/en/music.yml | 2 + app/src/main/sentences/skill_definitions.yml | 10 ++ 7 files changed, 196 insertions(+) create mode 100644 app/src/main/kotlin/org/stypox/dicio/skills/music/MusicInfo.kt create mode 100644 app/src/main/kotlin/org/stypox/dicio/skills/music/MusicOutput.kt create mode 100644 app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt create mode 100644 app/src/main/sentences/en/music.yml diff --git a/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt b/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt index 506955ae..b6688cf9 100644 --- a/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt +++ b/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt @@ -31,6 +31,7 @@ import org.stypox.dicio.skills.timer.TimerInfo import org.stypox.dicio.skills.translation.TranslationInfo import org.stypox.dicio.skills.weather.WeatherInfo import org.stypox.dicio.skills.joke.JokeInfo +import org.stypox.dicio.skills.music.MusicInfo import javax.inject.Inject import javax.inject.Singleton @@ -52,6 +53,7 @@ class SkillHandler @Inject constructor( TimerInfo, CurrentTimeInfo, MediaInfo, + MusicInfo, JokeInfo, ListeningInfo(dataStore), TranslationInfo, diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicInfo.kt b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicInfo.kt new file mode 100644 index 00000000..86704fcd --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicInfo.kt @@ -0,0 +1,39 @@ +package org.stypox.dicio.skills.music + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Notes +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.automirrored.filled.PlaylistPlay +import androidx.compose.material.icons.automirrored.filled.QueueMusic +import androidx.compose.material.icons.filled.Directions +import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.fragment.app.Fragment +import org.dicio.skill.skill.Skill +import org.dicio.skill.context.SkillContext +import org.dicio.skill.skill.SkillInfo +import org.stypox.dicio.R +import org.stypox.dicio.sentences.Sentences +import org.stypox.dicio.skills.open.OpenSkill + +object MusicInfo : SkillInfo("music") { + override fun name(context: Context) = + context.getString(R.string.skill_name_music) + + override fun sentenceExample(context: Context) = + context.getString(R.string.skill_sentence_example_music) + + @Composable + override fun icon() = + rememberVectorPainter(Icons.AutoMirrored.Filled.QueueMusic) + + override fun isAvailable(ctx: SkillContext): Boolean { + return Sentences.Music[ctx.sentencesLanguage] != null + } + + override fun build(ctx: SkillContext): Skill<*> { + return MusicSkill(MusicInfo, Sentences.Music[ctx.sentencesLanguage]!!) + } +} diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicOutput.kt b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicOutput.kt new file mode 100644 index 00000000..cd7fcb9d --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicOutput.kt @@ -0,0 +1,92 @@ +package org.stypox.dicio.skills.music + +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 = MusicOutput::class.simpleName + +class MusicOutput( + private val appName: String?, + private val packageName: String?, +) : SkillOutput { + override fun getSpeechOutput(ctx: SkillContext): String = if (packageName == null) { + ctx.getString(R.string.skill_music_no_app_found) + } else { + ctx.getString(R.string.skill_open_opening, appName) + } + + @Composable + override fun GraphicalOutput(ctx: SkillContext) { + if (appName == null || packageName == null) { + 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(), + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt new file mode 100644 index 00000000..0d06d960 --- /dev/null +++ b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt @@ -0,0 +1,48 @@ +package org.stypox.dicio.skills.music + +import android.app.SearchManager +import android.content.Intent +import android.content.pm.PackageManager +import android.provider.MediaStore +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.Music + +class MusicSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData) : + StandardRecognizerSkill(correspondingSkillInfo, data) { + + override suspend fun generateOutput(ctx: SkillContext, inputData: Music): SkillOutput { + val (song, artist) = when (inputData) { + is Music.Query -> Pair(inputData.song, inputData.artist) + } + + val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH).apply { + putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Media.ENTRY_CONTENT_TYPE) + putExtra(MediaStore.EXTRA_MEDIA_TITLE, song) + putExtra(SearchManager.QUERY, song) + } + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + // also search for the artist if given + if (artist != null) + intent.apply { + putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist) + putExtra(SearchManager.QUERY, "$song $artist") + } + + val packageManager: PackageManager = ctx.android.packageManager + val componentName = intent.resolveActivity(packageManager) + if (componentName == null) { + return MusicOutput(appName = null, packageName = null) + } + ctx.android.startActivity(intent) + + val applicationInfo = packageManager.getApplicationInfo(componentName.packageName, 0) + return MusicOutput( + appName = applicationInfo.loadLabel(packageManager).toString(), + packageName = applicationInfo.packageName, + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8230af59..dd38fc95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -246,4 +246,7 @@ Failed to copy to clipboard Auto DuckDuckGo did not provide results, asking for a Captcha to be solved + No music player found + Play Music + Play We will rock you by Queen diff --git a/app/src/main/sentences/en/music.yml b/app/src/main/sentences/en/music.yml new file mode 100644 index 00000000..03febd06 --- /dev/null +++ b/app/src/main/sentences/en/music.yml @@ -0,0 +1,2 @@ +query: + - play .song. (by .artist.)? diff --git a/app/src/main/sentences/skill_definitions.yml b/app/src/main/sentences/skill_definitions.yml index 7aa23f72..600d5234 100644 --- a/app/src/main/sentences/skill_definitions.yml +++ b/app/src/main/sentences/skill_definitions.yml @@ -37,6 +37,16 @@ skills: - id: where type: string + - id: music + specificity: high + sentences: + - id: query + captures: + - id: song + type: string + - id: artist + type: string + - id: media specificity: high sentences: