diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java index 8dd825a3f7..16bc8e2258 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java @@ -115,8 +115,9 @@ public interface VoiceManager { * Determines the preferred voice for the currently set locale * * @param voices a set of voices to chose from - * @return the preferred voice for the current locale + * @return the preferred voice for the current locale, or null if no voice can be found */ + @Nullable Voice getPreferredVoice(Set voices); /** diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java index 21d73d08e7..fa4ccdfbae 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java @@ -112,7 +112,7 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio if (defaultVoice == null) { TTSService tts = voiceManager.getTTS(); if (tts != null) { - defaultVoice = voiceManager.getPreferredVoice(tts.getAvailableVoices()); + return voiceManager.getPreferredVoice(tts.getAvailableVoices()); } } return defaultVoice; diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java index 7c87afaeb1..ed2047e838 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java @@ -12,8 +12,6 @@ */ package org.openhab.core.voice.internal; -import static java.util.stream.Collectors.*; - import java.io.IOException; import java.net.URI; import java.util.ArrayList; @@ -28,7 +26,9 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioManager; @@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory; @Component(immediate = true, configurationPid = VoiceManagerImpl.CONFIGURATION_PID, // property = Constants.SERVICE_PID + "=org.openhab.voice") @ConfigurableService(category = "system", label = "Voice", description_uri = VoiceManagerImpl.CONFIG_URI) +@NonNullByDefault public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { public static final String CONFIGURATION_PID = "org.openhab.voice"; @@ -107,12 +108,12 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { * default settings filled through the service configuration */ private String keyword = DEFAULT_KEYWORD; - private String listeningItem; - private String defaultTTS; - private String defaultSTT; - private String defaultKS; - private String defaultHLI; - private String defaultVoice; + private @Nullable String listeningItem; + private @Nullable String defaultTTS; + private @Nullable String defaultSTT; + private @Nullable String defaultKS; + private @Nullable String defaultHLI; + private @Nullable String defaultVoice; private final Map defaultVoices = new HashMap<>(); @Activate @@ -128,6 +129,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { modified(config); } + @SuppressWarnings("null") @Modified protected void modified(Map config) { if (config != null) { @@ -157,7 +159,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public void say(String text, PercentType volume) { + public void say(String text, @Nullable PercentType volume) { say(text, null, null, volume); } @@ -167,17 +169,17 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public void say(String text, String voiceId, PercentType volume) { + public void say(String text, @Nullable String voiceId, @Nullable PercentType volume) { say(text, voiceId, null, volume); } @Override - public void say(String text, String voiceId, String sinkId) { + public void say(String text, @Nullable String voiceId, @Nullable String sinkId) { say(text, voiceId, sinkId, null); } @Override - public void say(String text, String voiceId, String sinkId, PercentType volume) { + public void say(String text, @Nullable String voiceId, @Nullable String sinkId, @Nullable PercentType volume) { Objects.requireNonNull(text, "Text cannot be said as it is null."); try { @@ -265,7 +267,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public String interpret(String text, String hliId) throws InterpretationException { + public String interpret(String text, @Nullable String hliId) throws InterpretationException { HumanLanguageInterpreter interpreter; if (hliId == null) { interpreter = getHLI(); @@ -281,7 +283,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { return interpreter.interpret(localeProvider.getLocale(), text); } - private Voice getVoice(String id) { + private @Nullable Voice getVoice(String id) { if (id.contains(":")) { // it is a fully qualified unique id String[] segments = id.split(":"); @@ -299,7 +301,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { return null; } - private Voice getVoice(Set voices, String id) { + private @Nullable Voice getVoice(Set voices, String id) { for (Voice voice : voices) { if (voice.getUID().endsWith(":" + id)) { return voice; @@ -308,7 +310,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { return null; } - public static AudioFormat getPreferredFormat(Set audioFormats) { + public static @Nullable AudioFormat getPreferredFormat(Set audioFormats) { // Return the first concrete AudioFormat found for (AudioFormat currentAudioFormat : audioFormats) { // Check if currentAudioFormat is abstract @@ -403,7 +405,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { return null; } - public static AudioFormat getBestMatch(Set inputs, Set outputs) { + public static @Nullable AudioFormat getBestMatch(Set inputs, Set outputs) { AudioFormat preferredFormat = getPreferredFormat(inputs); for (AudioFormat output : outputs) { if (output.isCompatible(preferredFormat)) { @@ -420,7 +422,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public Voice getPreferredVoice(Set voices) { + public @Nullable Voice getPreferredVoice(Set voices) { // Express preferences with a Language Priority List Locale locale = localeProvider.getLocale(); @@ -436,9 +438,12 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { Locale preferredLocale = Locale.lookup(languageRanges, locales); // As a last resort choose some Locale - if (preferredLocale == null) { + if (preferredLocale == null && !voices.isEmpty()) { preferredLocale = locales.iterator().next(); } + if (preferredLocale == null) { + return null; + } // Determine preferred voice Voice preferredVoice = null; @@ -447,7 +452,6 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { preferredVoice = currentVoice; } } - assert (preferredVoice != null); // Return preferred voice return preferredVoice; @@ -459,9 +463,10 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public void startDialog(KSService ksService, STTService sttService, TTSService ttsService, - HumanLanguageInterpreter interpreter, AudioSource audioSource, AudioSink audioSink, Locale locale, - String keyword, String listeningItem) { + public void startDialog(@Nullable KSService ksService, @Nullable STTService sttService, + @Nullable TTSService ttsService, @Nullable HumanLanguageInterpreter interpreter, + @Nullable AudioSource audioSource, @Nullable AudioSink audioSink, @Nullable Locale locale, + @Nullable String keyword, @Nullable String listeningItem) { // use defaults, if null KSService ks = (ksService == null) ? getKS() : ksService; STTService stt = (sttService == null) ? getSTT() : sttService; @@ -522,7 +527,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public TTSService getTTS() { + public @Nullable TTSService getTTS() { TTSService tts = null; if (defaultTTS != null) { tts = ttsServices.get(defaultTTS); @@ -538,11 +543,11 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public TTSService getTTS(String id) { + public @Nullable TTSService getTTS(String id) { return ttsServices.get(id); } - private TTSService getTTS(Voice voice) { + private @Nullable TTSService getTTS(Voice voice) { return getTTS(voice.getUID().split(":")[0]); } @@ -552,7 +557,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public STTService getSTT() { + public @Nullable STTService getSTT() { STTService stt = null; if (defaultTTS != null) { stt = sttServices.get(defaultSTT); @@ -568,7 +573,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public STTService getSTT(String id) { + public @Nullable STTService getSTT(String id) { return sttServices.get(id); } @@ -578,7 +583,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public KSService getKS() { + public @Nullable KSService getKS() { KSService ks = null; if (defaultKS != null) { ks = ksServices.get(defaultKS); @@ -594,7 +599,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public KSService getKS(String id) { + public @Nullable KSService getKS(String id) { return ksServices.get(id); } @@ -604,7 +609,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public HumanLanguageInterpreter getHLI() { + public @Nullable HumanLanguageInterpreter getHLI() { HumanLanguageInterpreter hli = null; if (defaultHLI != null) { hli = humanLanguageInterpreters.get(defaultHLI); @@ -620,7 +625,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public HumanLanguageInterpreter getHLI(String id) { + public @Nullable HumanLanguageInterpreter getHLI(String id) { return humanLanguageInterpreters.get(id); } @@ -636,8 +641,8 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { private Set getAllVoicesSorted(Locale locale) { return ttsServices.values().stream().map(s -> s.getAvailableVoices()).flatMap(Collection::stream) - .sorted(createVoiceComparator(locale)) - .collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); + .sorted(createVoiceComparator(locale)).collect(Collectors + .collectingAndThen(Collectors.toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); } /** @@ -653,7 +658,10 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { */ private Comparator createVoiceComparator(Locale locale) { Comparator byTTSLabel = (Voice v1, Voice v2) -> { - return getTTS(v1).getLabel(locale).compareToIgnoreCase(getTTS(v2).getLabel(locale)); + TTSService tts1 = getTTS(v1); + TTSService tts2 = getTTS(v2); + return (tts1 == null || tts2 == null) ? 0 + : tts1.getLabel(locale).compareToIgnoreCase(tts2.getLabel(locale)); }; Comparator byVoiceLocale = (Voice v1, Voice v2) -> { return v1.getLocale().getDisplayName(locale).compareToIgnoreCase(v2.getLocale().getDisplayName(locale)); @@ -663,37 +671,42 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { @Override public @Nullable Voice getDefaultVoice() { - return defaultVoice != null ? getVoice(defaultVoice) : null; + String localDefaultVoice = defaultVoice; + return localDefaultVoice != null ? getVoice(localDefaultVoice) : null; } @Override - public Collection getParameterOptions(URI uri, String param, @Nullable String context, + public @Nullable Collection getParameterOptions(URI uri, String param, @Nullable String context, @Nullable Locale locale) { if (CONFIG_URI.equals(uri.toString())) { - if (CONFIG_DEFAULT_HLI.equals(param)) { - return humanLanguageInterpreters.values().stream() - .sorted((hli1, hli2) -> hli1.getLabel(locale).compareToIgnoreCase(hli2.getLabel(locale))) - .map(hli -> new ParameterOption(hli.getId(), hli.getLabel(locale))).collect(toList()); - } else if (CONFIG_DEFAULT_KS.equals(param)) { - return ksServices.values().stream() - .sorted((ks1, ks2) -> ks1.getLabel(locale).compareToIgnoreCase(ks2.getLabel(locale))) - .map(ks -> new ParameterOption(ks.getId(), ks.getLabel(locale))).collect(toList()); - } else if (CONFIG_DEFAULT_STT.equals(param)) { - return sttServices.values().stream() - .sorted((stt1, stt2) -> stt1.getLabel(locale).compareToIgnoreCase(stt2.getLabel(locale))) - .map(stt -> new ParameterOption(stt.getId(), stt.getLabel(locale))).collect(toList()); - } else if (CONFIG_DEFAULT_TTS.equals(param)) { - return ttsServices.values().stream() - .sorted((tts1, tts2) -> tts1.getLabel(locale).compareToIgnoreCase(tts2.getLabel(locale))) - .map(tts -> new ParameterOption(tts.getId(), tts.getLabel(locale))).collect(toList()); - } else if (CONFIG_DEFAULT_VOICE.equals(param)) { - Locale nullSafeLocale = locale != null ? locale : localeProvider.getLocale(); - return getAllVoicesSorted(nullSafeLocale) - .stream().filter(v -> getTTS(v) != null).map( - v -> new ParameterOption(v.getUID(), - String.format("%s - %s - %s", getTTS(v).getLabel(nullSafeLocale), - v.getLocale().getDisplayName(nullSafeLocale), v.getLabel()))) - .collect(toList()); + switch (param) { + case CONFIG_DEFAULT_HLI: + return humanLanguageInterpreters.values().stream() + .sorted((hli1, hli2) -> hli1.getLabel(locale).compareToIgnoreCase(hli2.getLabel(locale))) + .map(hli -> new ParameterOption(hli.getId(), hli.getLabel(locale))) + .collect(Collectors.toList()); + case CONFIG_DEFAULT_KS: + return ksServices.values().stream() + .sorted((ks1, ks2) -> ks1.getLabel(locale).compareToIgnoreCase(ks2.getLabel(locale))) + .map(ks -> new ParameterOption(ks.getId(), ks.getLabel(locale))) + .collect(Collectors.toList()); + case CONFIG_DEFAULT_STT: + return sttServices.values().stream() + .sorted((stt1, stt2) -> stt1.getLabel(locale).compareToIgnoreCase(stt2.getLabel(locale))) + .map(stt -> new ParameterOption(stt.getId(), stt.getLabel(locale))) + .collect(Collectors.toList()); + case CONFIG_DEFAULT_TTS: + return ttsServices.values().stream() + .sorted((tts1, tts2) -> tts1.getLabel(locale).compareToIgnoreCase(tts2.getLabel(locale))) + .map(tts -> new ParameterOption(tts.getId(), tts.getLabel(locale))) + .collect(Collectors.toList()); + case CONFIG_DEFAULT_VOICE: + Locale nullSafeLocale = locale != null ? locale : localeProvider.getLocale(); + return getAllVoicesSorted(nullSafeLocale).stream().filter(v -> getTTS(v) != null) + .map(v -> new ParameterOption(v.getUID(), + String.format("%s - %s - %s", getTTS(v).getLabel(nullSafeLocale), + v.getLocale().getDisplayName(nullSafeLocale), v.getLabel()))) + .collect(Collectors.toList()); } } return null; diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java index b4aa72e4f5..1cc1f89c97 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java @@ -23,12 +23,14 @@ import java.util.Collection; import java.util.Dictionary; import java.util.Hashtable; import java.util.Locale; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.core.audio.AudioManager; import org.openhab.core.config.core.ParameterOption; import org.openhab.core.test.java.JavaOSGiTest; +import org.openhab.core.voice.Voice; import org.openhab.core.voice.VoiceManager; import org.openhab.core.voice.text.InterpretationException; import org.osgi.framework.BundleContext; @@ -373,4 +375,16 @@ public class VoiceManagerImplTest extends JavaOSGiTest { assertTrue(isVoiceStubInTheOptions); } + + @Test + public void getPreferredVoiceOfAvailableTTSService() { + Voice voice = voiceManager.getPreferredVoice(ttsService.getAvailableVoices()); + assertNotNull(voice); + } + + @Test + public void getPreferredVoiceOfEmptySet() { + Voice voice = voiceManager.getPreferredVoice(Set.of()); + assertNull(voice); + } }