Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[voice] Changed annotation of getPreferredVoice method to allow null results #2186

Merged
merged 2 commits into from Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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<Voice> voices);

/**
Expand Down
Expand Up @@ -112,7 +112,7 @@ public void execute(String[] args, Console console) {
if (defaultVoice == null) {
TTSService tts = voiceManager.getTTS();
if (tts != null) {
defaultVoice = voiceManager.getPreferredVoice(tts.getAvailableVoices());
return voiceManager.getPreferredVoice(tts.getAvailableVoices());
}
}
return defaultVoice;
Expand Down
Expand Up @@ -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;
Expand All @@ -28,7 +26,9 @@
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;
Expand Down Expand Up @@ -73,6 +73,7 @@
@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";
Expand Down Expand Up @@ -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<String, String> defaultVoices = new HashMap<>();

@Activate
Expand All @@ -128,6 +129,7 @@ protected void activate(Map<String, Object> config) {
modified(config);
}

@SuppressWarnings("null")
@Modified
protected void modified(Map<String, Object> config) {
if (config != null) {
Expand Down Expand Up @@ -157,7 +159,7 @@ public void say(String text) {
}

@Override
public void say(String text, PercentType volume) {
public void say(String text, @Nullable PercentType volume) {
say(text, null, null, volume);
}

Expand All @@ -167,17 +169,17 @@ public void say(String text, String voiceId) {
}

@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 {
Expand Down Expand Up @@ -265,7 +267,7 @@ public String interpret(String text) throws InterpretationException {
}

@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();
Expand All @@ -281,7 +283,7 @@ public String interpret(String text, String hliId) throws InterpretationExceptio
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(":");
Expand All @@ -299,7 +301,7 @@ private Voice getVoice(String id) {
return null;
}

private Voice getVoice(Set<Voice> voices, String id) {
private @Nullable Voice getVoice(Set<Voice> voices, String id) {
for (Voice voice : voices) {
if (voice.getUID().endsWith(":" + id)) {
return voice;
Expand All @@ -308,7 +310,7 @@ private Voice getVoice(Set<Voice> voices, String id) {
return null;
}

public static AudioFormat getPreferredFormat(Set<AudioFormat> audioFormats) {
public static @Nullable AudioFormat getPreferredFormat(Set<AudioFormat> audioFormats) {
// Return the first concrete AudioFormat found
for (AudioFormat currentAudioFormat : audioFormats) {
// Check if currentAudioFormat is abstract
Expand Down Expand Up @@ -403,7 +405,7 @@ public static AudioFormat getPreferredFormat(Set<AudioFormat> audioFormats) {
return null;
}

public static AudioFormat getBestMatch(Set<AudioFormat> inputs, Set<AudioFormat> outputs) {
public static @Nullable AudioFormat getBestMatch(Set<AudioFormat> inputs, Set<AudioFormat> outputs) {
AudioFormat preferredFormat = getPreferredFormat(inputs);
for (AudioFormat output : outputs) {
if (output.isCompatible(preferredFormat)) {
Expand All @@ -420,7 +422,7 @@ public static AudioFormat getBestMatch(Set<AudioFormat> inputs, Set<AudioFormat>
}

@Override
public Voice getPreferredVoice(Set<Voice> voices) {
public @Nullable Voice getPreferredVoice(Set<Voice> voices) {
// Express preferences with a Language Priority List
Locale locale = localeProvider.getLocale();

Expand All @@ -436,9 +438,12 @@ public Voice getPreferredVoice(Set<Voice> voices) {
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;
Expand All @@ -447,7 +452,6 @@ public Voice getPreferredVoice(Set<Voice> voices) {
preferredVoice = currentVoice;
}
}
assert (preferredVoice != null);

// Return preferred voice
return preferredVoice;
Expand All @@ -459,9 +463,10 @@ public void startDialog() {
}

@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;
Expand Down Expand Up @@ -522,7 +527,7 @@ protected void removeHumanLanguageInterpreter(HumanLanguageInterpreter humanLang
}

@Override
public TTSService getTTS() {
public @Nullable TTSService getTTS() {
TTSService tts = null;
if (defaultTTS != null) {
tts = ttsServices.get(defaultTTS);
Expand All @@ -538,11 +543,11 @@ public TTSService getTTS() {
}

@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]);
}

Expand All @@ -552,7 +557,7 @@ public Collection<TTSService> getTTSs() {
}

@Override
public STTService getSTT() {
public @Nullable STTService getSTT() {
STTService stt = null;
if (defaultTTS != null) {
stt = sttServices.get(defaultSTT);
Expand All @@ -568,7 +573,7 @@ public STTService getSTT() {
}

@Override
public STTService getSTT(String id) {
public @Nullable STTService getSTT(String id) {
return sttServices.get(id);
}

Expand All @@ -578,7 +583,7 @@ public Collection<STTService> getSTTs() {
}

@Override
public KSService getKS() {
public @Nullable KSService getKS() {
KSService ks = null;
if (defaultKS != null) {
ks = ksServices.get(defaultKS);
Expand All @@ -594,7 +599,7 @@ public KSService getKS() {
}

@Override
public KSService getKS(String id) {
public @Nullable KSService getKS(String id) {
return ksServices.get(id);
}

Expand All @@ -604,7 +609,7 @@ public Collection<KSService> getKSs() {
}

@Override
public HumanLanguageInterpreter getHLI() {
public @Nullable HumanLanguageInterpreter getHLI() {
HumanLanguageInterpreter hli = null;
if (defaultHLI != null) {
hli = humanLanguageInterpreters.get(defaultHLI);
Expand All @@ -620,7 +625,7 @@ public HumanLanguageInterpreter getHLI() {
}

@Override
public HumanLanguageInterpreter getHLI(String id) {
public @Nullable HumanLanguageInterpreter getHLI(String id) {
return humanLanguageInterpreters.get(id);
}

Expand All @@ -636,8 +641,8 @@ public Set<Voice> getAllVoices() {

private Set<Voice> 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));
}

/**
Expand All @@ -653,7 +658,10 @@ private Set<Voice> getAllVoicesSorted(Locale locale) {
*/
private Comparator<Voice> createVoiceComparator(Locale locale) {
Comparator<Voice> 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<Voice> byVoiceLocale = (Voice v1, Voice v2) -> {
return v1.getLocale().getDisplayName(locale).compareToIgnoreCase(v2.getLocale().getDisplayName(locale));
Expand All @@ -663,37 +671,42 @@ private Comparator<Voice> createVoiceComparator(Locale locale) {

@Override
public @Nullable Voice getDefaultVoice() {
return defaultVoice != null ? getVoice(defaultVoice) : null;
String localDefaultVoice = defaultVoice;
return localDefaultVoice != null ? getVoice(localDefaultVoice) : null;
}

@Override
public Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
public @Nullable Collection<ParameterOption> 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;
Expand Down
Expand Up @@ -23,12 +23,14 @@
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;
Expand Down Expand Up @@ -373,4 +375,16 @@ public void getParameterOptionsForTheDefaultVoice() throws URISyntaxException {

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);
}
}