mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#660 Updates audio processing subsystem to use audio segments. Updates audio playback, recording and streaming to use audio segments. Adds support to UserPreferences for sound card selection and audio tone insertion during playback. Adds record preference for WAVE or MP3. Resolves issues with metadata in recordings. (#675)
This commit is contained in:
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.preference;
|
||||
@@ -32,6 +29,8 @@ public enum PreferenceType
|
||||
IDENTIFIER,
|
||||
JMBE_LIBRARY,
|
||||
MULTI_FREQUENCY,
|
||||
PLAYBACK,
|
||||
RADIO_REFERENCE,
|
||||
RECORD,
|
||||
TUNER;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.preference;
|
||||
@@ -27,12 +24,15 @@ import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
|
||||
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
||||
import io.github.dsheirer.preference.event.DecodeEventPreference;
|
||||
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
||||
import io.github.dsheirer.preference.playback.PlaybackPreference;
|
||||
import io.github.dsheirer.preference.playlist.PlaylistPreference;
|
||||
import io.github.dsheirer.preference.radioreference.RadioReferencePreference;
|
||||
import io.github.dsheirer.preference.record.RecordPreference;
|
||||
import io.github.dsheirer.preference.source.ChannelMultiFrequencyPreference;
|
||||
import io.github.dsheirer.preference.source.TunerPreference;
|
||||
import io.github.dsheirer.preference.swing.SwingPreference;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
|
||||
/**
|
||||
* User Preferences. A collection of preferences that can be accessed by preference type.
|
||||
@@ -56,8 +56,10 @@ public class UserPreferences implements Listener<PreferenceType>
|
||||
private DecodeEventPreference mDecodeEventPreference;
|
||||
private DirectoryPreference mDirectoryPreference;
|
||||
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
|
||||
private PlaybackPreference mPlaybackPreference;
|
||||
private PlaylistPreference mPlaylistPreference;
|
||||
private RadioReferencePreference mRadioReferencePreference;
|
||||
private RecordPreference mRecordPreference;
|
||||
private TalkgroupFormatPreference mTalkgroupFormatPreference;
|
||||
private TunerPreference mTunerPreference;
|
||||
private SwingPreference mSwingPreference = new SwingPreference();
|
||||
@@ -102,6 +104,14 @@ public class UserPreferences implements Listener<PreferenceType>
|
||||
return mChannelMultiFrequencyPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio playback preferences
|
||||
*/
|
||||
public PlaybackPreference getPlaybackPreference()
|
||||
{
|
||||
return mPlaybackPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist preferences
|
||||
*/
|
||||
@@ -118,6 +128,14 @@ public class UserPreferences implements Listener<PreferenceType>
|
||||
return mRadioReferencePreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording preferences
|
||||
*/
|
||||
public RecordPreference getRecordPreference()
|
||||
{
|
||||
return mRecordPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier preferences
|
||||
*/
|
||||
@@ -152,8 +170,10 @@ public class UserPreferences implements Listener<PreferenceType>
|
||||
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
|
||||
mDirectoryPreference = new DirectoryPreference(this::receive);
|
||||
mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive);
|
||||
mPlaybackPreference = new PlaybackPreference(this::receive);
|
||||
mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference);
|
||||
mRadioReferencePreference = new RadioReferencePreference(this::receive);
|
||||
mRecordPreference = new RecordPreference(this::receive);
|
||||
mTalkgroupFormatPreference = new TalkgroupFormatPreference(this::receive);
|
||||
mTunerPreference = new TunerPreference(this::receive);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.preference.playback;
|
||||
|
||||
import io.github.dsheirer.gui.preference.playback.ToneFrequency;
|
||||
import io.github.dsheirer.gui.preference.playback.ToneUtil;
|
||||
import io.github.dsheirer.gui.preference.playback.ToneVolume;
|
||||
import io.github.dsheirer.preference.Preference;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* User preferences for audio playback
|
||||
*/
|
||||
public class PlaybackPreference extends Preference
|
||||
{
|
||||
private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE = "audio.playback.segment.preempt.tone";
|
||||
private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE = "audio.playback.segment.start.tone";
|
||||
private static final String PREFERENCE_KEY_START_TONE_FREQUENCY = "audio.playback.segment.start.frequency";
|
||||
private static final String PREFERENCE_KEY_START_TONE_VOLUME = "audio.playback.segment.start.volume";
|
||||
private static final String PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY = "audio.playback.segment.preempt.frequency";
|
||||
private static final String PREFERENCE_KEY_PREEMPT_TONE_VOLUME = "audio.playback.segment.preempt.volume";
|
||||
private static final String PREFERENCE_KEY_MIXER_CHANNEL_CONFIG = "audio.playback.mixer.channel.configuration";
|
||||
private static final int TONE_LENGTH_SAMPLES = 180;
|
||||
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreference.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(PlaybackPreference.class);
|
||||
private Boolean mUseAudioSegmentStartTone;
|
||||
private Boolean mUseAudioSegmentPreemptTone;
|
||||
private ToneFrequency mStartToneFrequency;
|
||||
private ToneVolume mStartToneVolume;
|
||||
private ToneFrequency mPreemptToneFrequency;
|
||||
private ToneVolume mPreemptToneVolume;
|
||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
||||
|
||||
/**
|
||||
* Constructs this preference with an update listener
|
||||
* @param updateListener to receive notifications whenever these preferences change
|
||||
*/
|
||||
public PlaybackPreference(Listener<PreferenceType> updateListener)
|
||||
{
|
||||
super(updateListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceType getPreferenceType()
|
||||
{
|
||||
return PreferenceType.PLAYBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an audio segment start tone should be used.
|
||||
*/
|
||||
public boolean getUseAudioSegmentStartTone()
|
||||
{
|
||||
if(mUseAudioSegmentStartTone == null)
|
||||
{
|
||||
mUseAudioSegmentStartTone = mPreferences.getBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE, true);
|
||||
}
|
||||
|
||||
return mUseAudioSegmentStartTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preference for using an audio segment start tone
|
||||
*/
|
||||
public void setUseAudioSegmentStartTone(boolean use)
|
||||
{
|
||||
mUseAudioSegmentStartTone = use;
|
||||
mPreferences.putBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE, use);
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an audio segment preempt tone should be used.
|
||||
*/
|
||||
public boolean getUseAudioSegmentPreemptTone()
|
||||
{
|
||||
if(mUseAudioSegmentPreemptTone == null)
|
||||
{
|
||||
mUseAudioSegmentPreemptTone = mPreferences.getBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE, true);
|
||||
}
|
||||
|
||||
return mUseAudioSegmentPreemptTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preference for using an audio segment preempt tone
|
||||
*/
|
||||
public void setUseAudioSegmentPreemptTone(boolean use)
|
||||
{
|
||||
mUseAudioSegmentPreemptTone = use;
|
||||
mPreferences.putBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE, use);
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency for the start tone
|
||||
*/
|
||||
public ToneFrequency getStartToneFrequency()
|
||||
{
|
||||
if(mStartToneFrequency == null)
|
||||
{
|
||||
int frequency = mPreferences.getInt(PREFERENCE_KEY_START_TONE_FREQUENCY, ToneFrequency.F700.getValue());
|
||||
mStartToneFrequency = ToneFrequency.fromValue(frequency);
|
||||
}
|
||||
|
||||
return mStartToneFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency for the start tone
|
||||
*/
|
||||
public void setStartToneFrequency(ToneFrequency toneFrequency)
|
||||
{
|
||||
mStartToneFrequency = toneFrequency;
|
||||
mPreferences.putInt(PREFERENCE_KEY_START_TONE_FREQUENCY, toneFrequency.getValue());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency for the preempt tone
|
||||
*/
|
||||
public ToneFrequency getPreemptToneFrequency()
|
||||
{
|
||||
if(mPreemptToneFrequency == null)
|
||||
{
|
||||
int frequency = mPreferences.getInt(PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY, ToneFrequency.F400.getValue());
|
||||
mPreemptToneFrequency = ToneFrequency.fromValue(frequency);
|
||||
}
|
||||
|
||||
return mPreemptToneFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency for the preempt tone
|
||||
*/
|
||||
public void setPreemptToneFrequency(ToneFrequency toneFrequency)
|
||||
{
|
||||
mPreemptToneFrequency = toneFrequency;
|
||||
mPreferences.putInt(PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY, toneFrequency.getValue());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tone volume
|
||||
*/
|
||||
public ToneVolume getStartToneVolume()
|
||||
{
|
||||
if(mStartToneVolume == null)
|
||||
{
|
||||
int volume = mPreferences.getInt(PREFERENCE_KEY_START_TONE_VOLUME, ToneVolume.V3.getValue());
|
||||
mStartToneVolume = ToneVolume.fromValue(volume);
|
||||
}
|
||||
|
||||
return mStartToneVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the start tone volume
|
||||
*/
|
||||
public void setStartToneVolume(ToneVolume toneVolume)
|
||||
{
|
||||
mStartToneVolume = toneVolume;
|
||||
mPreferences.putInt(PREFERENCE_KEY_START_TONE_VOLUME, toneVolume.getValue());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preempt tone volume
|
||||
*/
|
||||
public ToneVolume getPreemptToneVolume()
|
||||
{
|
||||
if(mPreemptToneVolume == null)
|
||||
{
|
||||
int volume = mPreferences.getInt(PREFERENCE_KEY_PREEMPT_TONE_VOLUME, ToneVolume.V5.getValue());
|
||||
mPreemptToneVolume = ToneVolume.fromValue(volume);
|
||||
}
|
||||
|
||||
return mPreemptToneVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preempt tone volume
|
||||
*/
|
||||
public void setPreemptToneVolume(ToneVolume toneVolume)
|
||||
{
|
||||
mPreemptToneVolume = toneVolume;
|
||||
mPreferences.putInt(PREFERENCE_KEY_PREEMPT_TONE_VOLUME, toneVolume.getValue());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer with samples for the audio segment start tone
|
||||
*/
|
||||
public float[] getStartTone()
|
||||
{
|
||||
if(getUseAudioSegmentStartTone())
|
||||
{
|
||||
return ToneUtil.getTone(getStartToneFrequency(), getStartToneVolume(), TONE_LENGTH_SAMPLES);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer with samples for the audio segment preempt tone
|
||||
*/
|
||||
public float[] getPreemptTone()
|
||||
{
|
||||
if(getUseAudioSegmentPreemptTone())
|
||||
{
|
||||
return ToneUtil.getTone(getPreemptToneFrequency(), getPreemptToneVolume(), TONE_LENGTH_SAMPLES);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tone to use for testing the currently selected mixer output
|
||||
*/
|
||||
public float[] getMixerTestTone()
|
||||
{
|
||||
return ToneUtil.getTone(ToneFrequency.F1200, ToneVolume.V10, 800);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the preferred output mixer to use
|
||||
*/
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration()
|
||||
{
|
||||
if(mMixerChannelConfiguration == null)
|
||||
{
|
||||
MixerChannelConfiguration defaultConfig = MixerManager.getDefaultOutputMixer();
|
||||
|
||||
String configName = mPreferences.get(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, defaultConfig.toString());
|
||||
|
||||
for(MixerChannelConfiguration config: MixerManager.getOutputMixers())
|
||||
{
|
||||
if(config.toString().contentEquals(configName))
|
||||
{
|
||||
mMixerChannelConfiguration = config;
|
||||
}
|
||||
}
|
||||
|
||||
if(mMixerChannelConfiguration == null)
|
||||
{
|
||||
mMixerChannelConfiguration = defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return mMixerChannelConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred output mixer to use
|
||||
*/
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration configuration)
|
||||
{
|
||||
mMixerChannelConfiguration = configuration;
|
||||
mPreferences.put(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, configuration.toString());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.preference.record;
|
||||
|
||||
import io.github.dsheirer.preference.Preference;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* User preferences for playlists
|
||||
*/
|
||||
public class RecordPreference extends Preference
|
||||
{
|
||||
private static final String PREFERENCE_KEY_AUDIO_RECORD_FORMAT = "audio.record.format";
|
||||
private static final RecordFormat DEFAULT_RECORD_FORMAT = RecordFormat.MP3;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RecordPreference.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(RecordPreference.class);
|
||||
private RecordFormat mAudioRecordFormat;
|
||||
|
||||
/**
|
||||
* Constructs this preference with an update listener
|
||||
* @param updateListener to receive notifications whenever these preferences change
|
||||
*/
|
||||
public RecordPreference(Listener<PreferenceType> updateListener)
|
||||
{
|
||||
super(updateListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceType getPreferenceType()
|
||||
{
|
||||
return PreferenceType.RECORD;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Audio recording format
|
||||
*/
|
||||
public RecordFormat getAudioRecordFormat()
|
||||
{
|
||||
if(mAudioRecordFormat == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
String format = mPreferences.get(PREFERENCE_KEY_AUDIO_RECORD_FORMAT, DEFAULT_RECORD_FORMAT.name());
|
||||
mAudioRecordFormat = RecordFormat.valueOf(format);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error parsing record format preference", e);
|
||||
}
|
||||
|
||||
if(mAudioRecordFormat == null)
|
||||
{
|
||||
mAudioRecordFormat = DEFAULT_RECORD_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
return mAudioRecordFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio recording format
|
||||
*/
|
||||
public void setAudioRecordFormat(RecordFormat audioRecordFormat)
|
||||
{
|
||||
mAudioRecordFormat = audioRecordFormat;
|
||||
mPreferences.put(PREFERENCE_KEY_AUDIO_RECORD_FORMAT, audioRecordFormat.name());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user