From e1121390d631e82fd9dd4f5cd3f5529fd1ea5bb0 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 1/3] 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 5c7c7f885..8640e25a1 100644 --- a/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt +++ b/app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt @@ -33,6 +33,7 @@ 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.flashlight.FlashlightInfo +import org.stypox.dicio.skills.music.MusicInfo import javax.inject.Inject import javax.inject.Singleton @@ -54,6 +55,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 000000000..86704fcdf --- /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 000000000..cd7fcb9dc --- /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 000000000..0d06d9600 --- /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 ea8db6663..686c4a46c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -278,4 +278,7 @@ Privacy policy Dicio connects to external services only when a skill is expected to do so, or to download machine learning models during setup. All speech processing is performed locally on-device, and most skills can be used offline. https://stypox.org/dicio-privacy-policy.html + 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 000000000..03febd06e --- /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 ed347e967..c599b4ecb 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: From c4c15e271cabc22c01ce00d8a86d4810a304e695 Mon Sep 17 00:00:00 2001 From: Gabriel <69007475+Friendly-Banana@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:20:15 +0100 Subject: [PATCH 2/3] address review add intent docs add README + fastlane skill description change music skill strings --- README.md | 1 + .../stypox/dicio/skills/music/MusicOutput.kt | 21 ++++++++++++------- .../stypox/dicio/skills/music/MusicSkill.kt | 4 +++- app/src/main/res/values/strings.xml | 3 ++- app/src/main/sentences/en/music.yml | 1 + .../android/en-US/full_description.txt | 1 + 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 54b75feeb..b7a6fbb9c 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Currently Dicio answers questions about: - **navigation**: opens the navigation app at the requested position - _Take me to New York, fifteenth avenue_ - **jokes**: tells you a joke - _Tell me a joke_ - **media**: play, pause, previous, next song +- **music**: search for and play a specific song - **translation**: translate from/to any language with **Lingva** - _How do I say Football in German?_ - **wake word control**: turn on/off the wakeword - _Stop listening_ - **notifications**: reads all notifications currently in the status bar - _What are my notifications?_ 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 index cd7fcb9dc..02343ca12 100644 --- a/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicOutput.kt +++ b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicOutput.kt @@ -3,11 +3,13 @@ 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.BoxWithConstraints 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.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -30,11 +32,12 @@ private val TAG = MusicOutput::class.simpleName class MusicOutput( private val appName: String?, private val packageName: String?, + private val songName: 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) + ctx.getString(R.string.skill_music_playing, songName, appName) } @Composable @@ -58,13 +61,15 @@ class MusicOutput( } if (icon != null) { - Image( - painter = rememberDrawablePainter(icon), - contentDescription = appName, - modifier = Modifier - .fillMaxWidth(0.2f) - .aspectRatio(1.0f), - ) + BoxWithConstraints { + Image( + painter = rememberDrawablePainter(icon), + contentDescription = appName, + modifier = Modifier + .requiredWidth(minOf(maxWidth * 0.2f, 80.dp)) + .aspectRatio(1.0f), + ) + } Spacer(modifier = Modifier.width(8.dp)) } 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 index 0d06d9600..5f83143c5 100644 --- a/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt +++ b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt @@ -19,6 +19,7 @@ class MusicSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData is Music.Query -> Pair(inputData.song, inputData.artist) } + // https://developer.android.com/guide/components/intents-common#PlaySearch 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) @@ -35,7 +36,7 @@ class MusicSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData val packageManager: PackageManager = ctx.android.packageManager val componentName = intent.resolveActivity(packageManager) if (componentName == null) { - return MusicOutput(appName = null, packageName = null) + return MusicOutput(appName = null, packageName = null, songName = null) } ctx.android.startActivity(intent) @@ -43,6 +44,7 @@ class MusicSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData return MusicOutput( appName = applicationInfo.loadLabel(packageManager).toString(), packageName = applicationInfo.packageName, + songName = song ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 686c4a46c..aafa747d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -279,6 +279,7 @@ Dicio connects to external services only when a skill is expected to do so, or to download machine learning models during setup. All speech processing is performed locally on-device, and most skills can be used offline. https://stypox.org/dicio-privacy-policy.html No music player found - Play Music + Playing %1$s on %2$s + 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 index 03febd06e..5efbadec9 100644 --- a/app/src/main/sentences/en/music.yml +++ b/app/src/main/sentences/en/music.yml @@ -1,2 +1,3 @@ query: - play .song. (by .artist.)? + - start playing .song. (by .artist.)? diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 0542a8940..554ec8bd5 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -13,6 +13,7 @@ Dicio answers questions about:
  • navigation: opens the navigation app at the requested position - Take me to New York, fifteenth avenue
  • jokes: tells you a joke - Tell me a joke
  • media: play, pause, previous, next song - Next Song
  • +
  • music: search for and play a specific song - Play We will rock you by Queen
  • translation: translate from/to any language with Lingva - How do I say Football in German?
  • wake word control: turn on/off the wakeword - Stop listening
  • notifications: reads all notifications currently in the status bar - What are my notifications?
  • From 00572625f57ed1da77565b78b99c8a48009f9636 Mon Sep 17 00:00:00 2001 From: Gabriel <69007475+Friendly-Banana@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:44:40 +0100 Subject: [PATCH 3/3] improve intent building --- .../org/stypox/dicio/skills/music/MusicSkill.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 index 5f83143c5..833582354 100644 --- a/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt +++ b/app/src/main/kotlin/org/stypox/dicio/skills/music/MusicSkill.kt @@ -23,15 +23,15 @@ class MusicSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData 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 { + + if (artist != null) { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist) - putExtra(SearchManager.QUERY, "$song $artist") + putExtra(SearchManager.QUERY, "$artist $song") + } else { + putExtra(SearchManager.QUERY, song) } + } + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK val packageManager: PackageManager = ctx.android.packageManager val componentName = intent.resolveActivity(packageManager)