kopia lustrzana https://github.com/sh123/codec2_talkie
commit
ea2655515a
|
@ -50,7 +50,8 @@ It does not deal with radio management, modulation, etc, it is up to your modem
|
||||||
- Keep screen ON
|
- Keep screen ON
|
||||||
- **Codec2**
|
- **Codec2**
|
||||||
- Set Codec2 mode/speed from 450 up to 3200 bps
|
- Set Codec2 mode/speed from 450 up to 3200 bps
|
||||||
- Enable/disable loopback test mode
|
- Enable/disable echo/loopback test mode
|
||||||
|
- Enable/disable RX/TX recorder
|
||||||
- **TNC parameters**
|
- **TNC parameters**
|
||||||
- Change default baud rate for USB port
|
- Change default baud rate for USB port
|
||||||
- Set default Bluetooth device for automatic connectivity on startup
|
- Set default Bluetooth device for automatic connectivity on startup
|
||||||
|
@ -61,6 +62,7 @@ It does not deal with radio management, modulation, etc, it is up to your modem
|
||||||
- Enable/Disable KISS non-real time buffered playback mode
|
- Enable/Disable KISS non-real time buffered playback mode
|
||||||
- Enable/Disable KISS extensions for radio module control and signal levels (modem must support them to work correctly!)
|
- Enable/Disable KISS extensions for radio module control and signal levels (modem must support them to work correctly!)
|
||||||
- Set radio parameters (frequency, bandwidth, spreading factor, coding rate, power, sync word, crc checksum enable/disable)
|
- Set radio parameters (frequency, bandwidth, spreading factor, coding rate, power, sync word, crc checksum enable/disable)
|
||||||
|
- **Recording player**, simple player, which allows TX/RX recording playback and removal
|
||||||
|
|
||||||
# Suitable radios and modems
|
# Suitable radios and modems
|
||||||
- Tested, works:
|
- Tested, works:
|
||||||
|
|
|
@ -11,7 +11,7 @@ android {
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.3"
|
versionName "1.4"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.BluetoothSettingsActivity"
|
android:name=".settings.BluetoothSettingsActivity"
|
||||||
android:configChanges="orientation|screenSize" />
|
android:configChanges="orientation|screenSize" />
|
||||||
|
<activity
|
||||||
|
android:name=".recorder.RecorderActivity"
|
||||||
|
android:configChanges="orientation|screenSize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize">
|
android:configChanges="orientation|screenSize">
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package com.radio.codec2talkie;
|
package com.radio.codec2talkie;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.view.MenuCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
@ -23,7 +25,6 @@ import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -36,9 +37,11 @@ import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.radio.codec2talkie.audio.AudioProcessor;
|
||||||
import com.radio.codec2talkie.connect.BluetoothConnectActivity;
|
import com.radio.codec2talkie.connect.BluetoothConnectActivity;
|
||||||
import com.radio.codec2talkie.connect.SocketHandler;
|
import com.radio.codec2talkie.connect.SocketHandler;
|
||||||
import com.radio.codec2talkie.protocol.ProtocolFactory;
|
import com.radio.codec2talkie.protocol.ProtocolFactory;
|
||||||
|
import com.radio.codec2talkie.recorder.RecorderActivity;
|
||||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||||
import com.radio.codec2talkie.settings.SettingsActivity;
|
import com.radio.codec2talkie.settings.SettingsActivity;
|
||||||
import com.radio.codec2talkie.tools.RadioTools;
|
import com.radio.codec2talkie.tools.RadioTools;
|
||||||
|
@ -53,12 +56,11 @@ import java.util.Locale;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private static final String TAG = MainActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
private final static int REQUEST_CONNECT_BT = 1;
|
private final static int REQUEST_CONNECT_BT = 1;
|
||||||
private final static int REQUEST_CONNECT_USB = 2;
|
private final static int REQUEST_CONNECT_USB = 2;
|
||||||
private final static int REQUEST_PERMISSIONS = 3;
|
private final static int REQUEST_PERMISSIONS = 3;
|
||||||
private final static int REQUEST_SETTINGS = 4;
|
private final static int REQUEST_SETTINGS = 4;
|
||||||
|
private final static int REQUEST_VOICEMAIL = 5;
|
||||||
|
|
||||||
// S9 level at -93 dBm as per VHF Managers Handbook
|
// S9 level at -93 dBm as per VHF Managers Handbook
|
||||||
private final static int S_METER_S0_VALUE_DB = -153;
|
private final static int S_METER_S0_VALUE_DB = -153;
|
||||||
|
@ -90,6 +92,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
String appName = getResources().getString(R.string.app_name);
|
String appName = getResources().getString(R.string.app_name);
|
||||||
|
@ -166,8 +169,13 @@ public class MainActivity extends AppCompatActivity {
|
||||||
startActivityForResult(bluetoothConnectIntent, REQUEST_CONNECT_BT);
|
startActivityForResult(bluetoothConnectIntent, REQUEST_CONNECT_BT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void startVoicemailActivity() {
|
||||||
|
Intent voicemailIntent = new Intent(this, RecorderActivity.class);
|
||||||
|
startActivityForResult(voicemailIntent, REQUEST_VOICEMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean requestPermissions() {
|
protected boolean requestPermissions() {
|
||||||
List<String> permissionsToRequest = new LinkedList<String>();
|
List<String> permissionsToRequest = new LinkedList<>();
|
||||||
|
|
||||||
for (String permission : _requiredPermissions) {
|
for (String permission : _requiredPermissions) {
|
||||||
if (ContextCompat.checkSelfPermission(MainActivity.this, permission) == PackageManager.PERMISSION_DENIED) {
|
if (ContextCompat.checkSelfPermission(MainActivity.this, permission) == PackageManager.PERMISSION_DENIED) {
|
||||||
|
@ -215,6 +223,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
MenuCompat.setGroupDividerEnabled(menu, true);
|
||||||
getMenuInflater().inflate(R.menu.main_menu, menu);
|
getMenuInflater().inflate(R.menu.main_menu, menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -229,6 +238,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
startActivityForResult(settingsIntent, REQUEST_SETTINGS);
|
startActivityForResult(settingsIntent, REQUEST_SETTINGS);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (itemId == R.id.voicemail) {
|
||||||
|
startVoicemailActivity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if (itemId == R.id.exit) {
|
else if (itemId == R.id.exit) {
|
||||||
stopRunning();
|
stopRunning();
|
||||||
return true;
|
return true;
|
||||||
|
@ -303,7 +316,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
if (requestCode == REQUEST_PERMISSIONS) {
|
if (requestCode == REQUEST_PERMISSIONS) {
|
||||||
boolean allGranted = true;
|
boolean allGranted = true;
|
||||||
|
@ -427,9 +440,15 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
speedModeInfo += ", " + protocolType.toString();
|
speedModeInfo += ", " + protocolType.toString();
|
||||||
|
|
||||||
|
boolean voicemailEnabled = _sharedPreferences.getBoolean(PreferenceKeys.CODEC2_VOICEMAIL, false);
|
||||||
|
|
||||||
|
if (voicemailEnabled) {
|
||||||
|
speedModeInfo += ", " + getString(R.string.recorder_status_label);
|
||||||
|
}
|
||||||
_textCodecMode.setText(speedModeInfo);
|
_textCodecMode.setText(speedModeInfo);
|
||||||
|
|
||||||
_audioProcessor = new AudioProcessor(transportType, protocolType, codec2ModeId, onAudioProcessorStateChanged, getApplicationContext());
|
_audioProcessor = new AudioProcessor(transportType, protocolType, voicemailEnabled, codec2ModeId, onAudioProcessorStateChanged, getApplicationContext());
|
||||||
_audioProcessor.start();
|
_audioProcessor.start();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
package com.radio.codec2talkie.audio;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
import com.radio.codec2talkie.MainActivity;
|
||||||
|
import com.ustadmobile.codec2.Codec2;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class AudioPlayer extends Thread {
|
||||||
|
private static final String TAG = MainActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
public static final int PLAYER_STARTED = 1;
|
||||||
|
public static final int PLAYER_PLAYING_FILE = 2;
|
||||||
|
public static final int PLAYER_PLAYED_FILE= 3;
|
||||||
|
public static final int PLAYER_ERROR = 4;
|
||||||
|
public static final int PLAYER_STOPPED = 5;
|
||||||
|
|
||||||
|
private final Handler _onPlayerStateChanged;
|
||||||
|
private final Context _context;
|
||||||
|
private final File[] _files;
|
||||||
|
|
||||||
|
private final int AUDIO_SAMPLE_SIZE = 8000;
|
||||||
|
|
||||||
|
private AudioTrack _systemAudioPlayer;
|
||||||
|
private short[] _playbackAudioBuffer;
|
||||||
|
|
||||||
|
private long _codec2Con;
|
||||||
|
private int _codec2Mode;
|
||||||
|
|
||||||
|
private int _audioBufferSize;
|
||||||
|
private int _codec2FrameSize;
|
||||||
|
|
||||||
|
private int _currentStatus = PLAYER_STOPPED;
|
||||||
|
|
||||||
|
private boolean _stopPlayback = false;
|
||||||
|
|
||||||
|
public AudioPlayer(File[] files, Handler onPlayerStateChanged, Context context) {
|
||||||
|
|
||||||
|
_onPlayerStateChanged = onPlayerStateChanged;
|
||||||
|
_context = context;
|
||||||
|
_files = files;
|
||||||
|
_codec2Con = 0;
|
||||||
|
|
||||||
|
Arrays.sort(_files);
|
||||||
|
|
||||||
|
constructSystemAudioDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void constructSystemAudioDevices() {
|
||||||
|
int _audioPlayerMinBufferSize = AudioTrack.getMinBufferSize(
|
||||||
|
AUDIO_SAMPLE_SIZE,
|
||||||
|
AudioFormat.CHANNEL_OUT_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT);
|
||||||
|
_systemAudioPlayer = new AudioTrack.Builder()
|
||||||
|
.setAudioAttributes(new AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.build())
|
||||||
|
.setAudioFormat(new AudioFormat.Builder()
|
||||||
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
.setSampleRate(AUDIO_SAMPLE_SIZE)
|
||||||
|
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
||||||
|
.build())
|
||||||
|
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||||
|
.setBufferSizeInBytes(10 * _audioPlayerMinBufferSize)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void constructCodec2(int codecMode) {
|
||||||
|
if (_codec2Con != 0) {
|
||||||
|
Codec2.destroy(_codec2Con);
|
||||||
|
}
|
||||||
|
_codec2Con = Codec2.create(codecMode);
|
||||||
|
|
||||||
|
_audioBufferSize = Codec2.getSamplesPerFrame(_codec2Con);
|
||||||
|
_codec2FrameSize = Codec2.getBitsSize(_codec2Con); // returns number of bytes
|
||||||
|
|
||||||
|
_playbackAudioBuffer = new short[_audioBufferSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendStatusUpdate(int newStatus, String fileName) {
|
||||||
|
if (newStatus != _currentStatus) {
|
||||||
|
_currentStatus = newStatus;
|
||||||
|
Message msg = Message.obtain();
|
||||||
|
msg.what = newStatus;
|
||||||
|
msg.obj = fileName;
|
||||||
|
|
||||||
|
_onPlayerStateChanged.sendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean playFile(File file) {
|
||||||
|
String codec2ModeStr = file.getName().substring(0, 2);
|
||||||
|
int codec2Mode = Integer.parseInt(codec2ModeStr);
|
||||||
|
|
||||||
|
// reconstruct codec2 on mode change
|
||||||
|
if (_codec2Con == 0 || _codec2Mode != codec2Mode) {
|
||||||
|
_codec2Mode = codec2Mode;
|
||||||
|
constructCodec2(codec2Mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream inputStream;
|
||||||
|
try {
|
||||||
|
inputStream = new FileInputStream(file);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
sendStatusUpdate(PLAYER_ERROR, file.getName());
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] codec2Buffer = new byte[_codec2FrameSize];
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (inputStream.read(codec2Buffer) == _codec2FrameSize) {
|
||||||
|
if (_stopPlayback) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Codec2.decode(_codec2Con, _playbackAudioBuffer, codec2Buffer);
|
||||||
|
_systemAudioPlayer.write(_playbackAudioBuffer, 0, _audioBufferSize);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
sendStatusUpdate(PLAYER_ERROR, file.getName());
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void play() {
|
||||||
|
_systemAudioPlayer.play();
|
||||||
|
for (File file : _files) {
|
||||||
|
if (file.isDirectory() || !file.getName().endsWith(".c2")) continue;
|
||||||
|
sendStatusUpdate(PLAYER_PLAYING_FILE, file.getName());
|
||||||
|
if (!playFile(file)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sendStatusUpdate(PLAYER_PLAYED_FILE, file.getName());
|
||||||
|
}
|
||||||
|
_systemAudioPlayer.stop();
|
||||||
|
_systemAudioPlayer.release();
|
||||||
|
if (_codec2Con != 0) {
|
||||||
|
Codec2.destroy(_codec2Con);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
sendStatusUpdate(PLAYER_STARTED, null);
|
||||||
|
play();
|
||||||
|
sendStatusUpdate(PLAYER_STOPPED, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopPlayback() {
|
||||||
|
_stopPlayback = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.radio.codec2talkie;
|
package com.radio.codec2talkie.audio;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioAttributes;
|
import android.media.AudioAttributes;
|
||||||
|
@ -11,12 +11,8 @@ import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
@ -46,8 +42,8 @@ public class AudioProcessor extends Thread {
|
||||||
public static final int PROCESSOR_PROCESS = 11;
|
public static final int PROCESSOR_PROCESS = 11;
|
||||||
public static final int PROCESSOR_QUIT = 12;
|
public static final int PROCESSOR_QUIT = 12;
|
||||||
|
|
||||||
private static int AUDIO_MIN_LEVEL = -70;
|
private static final int AUDIO_MIN_LEVEL = -70;
|
||||||
private static int AUDIO_MAX_LEVEL = 0;
|
private static final int AUDIO_MAX_LEVEL = 0;
|
||||||
private final int AUDIO_SAMPLE_SIZE = 8000;
|
private final int AUDIO_SAMPLE_SIZE = 8000;
|
||||||
|
|
||||||
private final int PROCESS_INTERVAL_MS = 20;
|
private final int PROCESS_INTERVAL_MS = 20;
|
||||||
|
@ -86,13 +82,14 @@ public class AudioProcessor extends Thread {
|
||||||
private final Context _context;
|
private final Context _context;
|
||||||
|
|
||||||
public AudioProcessor(TransportFactory.TransportType transportType, ProtocolFactory.ProtocolType protocolType,
|
public AudioProcessor(TransportFactory.TransportType transportType, ProtocolFactory.ProtocolType protocolType,
|
||||||
int codec2Mode, Handler onPlayerStateChanged, Context context) throws IOException {
|
boolean voicemailEnabled, int codec2Mode,
|
||||||
|
Handler onPlayerStateChanged, Context context) throws IOException {
|
||||||
_onPlayerStateChanged = onPlayerStateChanged;
|
_onPlayerStateChanged = onPlayerStateChanged;
|
||||||
|
|
||||||
_context = context;
|
_context = context;
|
||||||
|
|
||||||
_transport = TransportFactory.create(transportType);
|
_transport = TransportFactory.create(transportType);
|
||||||
_protocol = ProtocolFactory.create(protocolType);
|
_protocol = ProtocolFactory.create(protocolType, codec2Mode, voicemailEnabled);
|
||||||
|
|
||||||
_processPeriodicTimer = new Timer();
|
_processPeriodicTimer = new Timer();
|
||||||
|
|
||||||
|
@ -340,8 +337,6 @@ public class AudioProcessor extends Thread {
|
||||||
// playback
|
// playback
|
||||||
if (_protocol.receive(_protocolReceiveCallback)) {
|
if (_protocol.receive(_protocolReceiveCallback)) {
|
||||||
sendStatusUpdate(PROCESSOR_RECEIVING);
|
sendStatusUpdate(PROCESSOR_RECEIVING);
|
||||||
} else {
|
|
||||||
// idling
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package com.radio.codec2talkie.protocol;
|
package com.radio.codec2talkie.protocol;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public abstract class Callback {
|
public abstract class Callback {
|
||||||
abstract protected void onReceiveAudioFrames(byte [] frame);
|
abstract protected void onReceiveAudioFrames(byte [] frame);
|
||||||
abstract protected void onReceiveSignalLevel(byte [] rawData);
|
abstract protected void onReceiveSignalLevel(byte [] rawData);
|
||||||
|
|
|
@ -8,7 +8,7 @@ public class ProtocolFactory {
|
||||||
KISS_BUFFERED("KISS BUFFERED"),
|
KISS_BUFFERED("KISS BUFFERED"),
|
||||||
KISS_PARROT("KISS PARROT");
|
KISS_PARROT("KISS PARROT");
|
||||||
|
|
||||||
private String _name;
|
private final String _name;
|
||||||
|
|
||||||
ProtocolType(String name) {
|
ProtocolType(String name) {
|
||||||
_name = name;
|
_name = name;
|
||||||
|
@ -20,17 +20,28 @@ public class ProtocolFactory {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Protocol create(ProtocolType protocolType) {
|
public static Protocol create(ProtocolType protocolType, int codec2ModeId, boolean voicemailEnabled) {
|
||||||
|
Protocol proto;
|
||||||
switch (protocolType) {
|
switch (protocolType) {
|
||||||
case KISS:
|
case KISS:
|
||||||
return new Kiss();
|
proto = new Kiss();
|
||||||
|
break;
|
||||||
case KISS_BUFFERED:
|
case KISS_BUFFERED:
|
||||||
return new KissBuffered();
|
proto = new KissBuffered();
|
||||||
|
break;
|
||||||
case KISS_PARROT:
|
case KISS_PARROT:
|
||||||
return new KissParrot();
|
proto = new KissParrot();
|
||||||
|
break;
|
||||||
case RAW:
|
case RAW:
|
||||||
default:
|
default:
|
||||||
return new Raw();
|
proto = new Raw();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (voicemailEnabled) {
|
||||||
|
proto = new VoicemailProxy(proto, codec2ModeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
package com.radio.codec2talkie.protocol;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.radio.codec2talkie.MainActivity;
|
||||||
|
import com.radio.codec2talkie.tools.StorageTools;
|
||||||
|
import com.radio.codec2talkie.transport.Transport;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
public class VoicemailProxy implements Protocol {
|
||||||
|
|
||||||
|
private static final String TAG = MainActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
private final int ROTATION_DELAY_MS = 10000;
|
||||||
|
|
||||||
|
Context _context;
|
||||||
|
File _storage;
|
||||||
|
FileOutputStream _activeStream;
|
||||||
|
Timer _fileRotationTimer;
|
||||||
|
|
||||||
|
final Protocol _protocol;
|
||||||
|
final int _codec2ModeId;
|
||||||
|
|
||||||
|
public VoicemailProxy(Protocol protocol, int codec2ModeId) {
|
||||||
|
_protocol = protocol;
|
||||||
|
_codec2ModeId = codec2ModeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(Transport transport, Context context) throws IOException {
|
||||||
|
_context = context;
|
||||||
|
_storage = StorageTools.getStorage(context);
|
||||||
|
_protocol.initialize(transport, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(byte[] frame) throws IOException {
|
||||||
|
_protocol.send(frame);
|
||||||
|
writeToFile(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean receive(Callback callback) throws IOException {
|
||||||
|
return _protocol.receive(new Callback() {
|
||||||
|
@Override
|
||||||
|
protected void onReceiveAudioFrames(byte[] audioFrames) {
|
||||||
|
callback.onReceiveAudioFrames(audioFrames);
|
||||||
|
writeToFile(audioFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onReceiveSignalLevel(byte[] rawData) {
|
||||||
|
callback.onReceiveSignalLevel(rawData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
_protocol.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeToFile(byte[] rawData) {
|
||||||
|
stopRotationTimer();
|
||||||
|
createStreamIfNotExists();
|
||||||
|
writeToStream(rawData);
|
||||||
|
startRotationTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeToStream(byte[] rawData) {
|
||||||
|
try {
|
||||||
|
if (_activeStream != null) {
|
||||||
|
_activeStream.write(rawData);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createStreamIfNotExists() {
|
||||||
|
if (_activeStream == null) {
|
||||||
|
try {
|
||||||
|
Date date = new Date();
|
||||||
|
File newDirectory = new File(_storage, getNewDirectoryName(date));
|
||||||
|
if (!newDirectory.exists() && !newDirectory.mkdirs()) {
|
||||||
|
Log.e(TAG, "Failed to create directory for voicemails");
|
||||||
|
}
|
||||||
|
File newAudioFile = new File(newDirectory, getNewFileName(date));
|
||||||
|
_activeStream = new FileOutputStream(newAudioFile);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNewDirectoryName(Date date) {
|
||||||
|
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd", Locale.US);
|
||||||
|
return df.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNewFileName(Date date) {
|
||||||
|
SimpleDateFormat tf = new SimpleDateFormat("HHmmss", Locale.ENGLISH);
|
||||||
|
String codec2mode = String.format(Locale.ENGLISH, "%02d", _codec2ModeId);
|
||||||
|
return codec2mode + "_" + tf.format(date) + ".c2";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startRotationTimer() {
|
||||||
|
_fileRotationTimer = new Timer();
|
||||||
|
_fileRotationTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (_activeStream != null) {
|
||||||
|
try {
|
||||||
|
_activeStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_activeStream = null;
|
||||||
|
}
|
||||||
|
}, ROTATION_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopRotationTimer() {
|
||||||
|
try {
|
||||||
|
if (_fileRotationTimer != null) {
|
||||||
|
_fileRotationTimer.cancel();
|
||||||
|
_fileRotationTimer.purge();
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
package com.radio.codec2talkie.recorder;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.radio.codec2talkie.MainActivity;
|
||||||
|
import com.radio.codec2talkie.R;
|
||||||
|
import com.radio.codec2talkie.audio.AudioPlayer;
|
||||||
|
import com.radio.codec2talkie.tools.StorageTools;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RecorderActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String TAG = MainActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
private File _root;
|
||||||
|
private File _currentDirectory;
|
||||||
|
private ArrayAdapter<Object> _dirAdapter;
|
||||||
|
private ListView _recordingList;
|
||||||
|
private TextView _textPlaybackStatus;
|
||||||
|
private AudioPlayer _audioPlayer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_recorder);
|
||||||
|
|
||||||
|
_dirAdapter = new ArrayAdapter<>(this, R.layout.support_simple_spinner_dropdown_item);
|
||||||
|
_recordingList = findViewById(R.id.listRecorder);
|
||||||
|
_recordingList.setOnItemClickListener(onFileClickListener);
|
||||||
|
_recordingList.setOnItemLongClickListener(onFileLongClickListener);
|
||||||
|
_recordingList.setAdapter(_dirAdapter);
|
||||||
|
|
||||||
|
_textPlaybackStatus = findViewById(R.id.textPlaybackStatus);
|
||||||
|
_textPlaybackStatus.setText(R.string.player_status_stopped);
|
||||||
|
|
||||||
|
_root = StorageTools.getStorage(getApplicationContext());
|
||||||
|
loadFiles(_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Handler onPlayerStateChanged = new Handler(Looper.getMainLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case AudioPlayer.PLAYER_STARTED:
|
||||||
|
_textPlaybackStatus.setText(R.string.player_status_started);
|
||||||
|
break;
|
||||||
|
case AudioPlayer.PLAYER_STOPPED:
|
||||||
|
_textPlaybackStatus.setText(getString(R.string.player_status_stopped));
|
||||||
|
_audioPlayer = null;
|
||||||
|
break;
|
||||||
|
case AudioPlayer.PLAYER_ERROR:
|
||||||
|
_textPlaybackStatus.setText(getString(R.string.player_status_error, msg.obj));
|
||||||
|
break;
|
||||||
|
case AudioPlayer.PLAYER_PLAYING_FILE:
|
||||||
|
_textPlaybackStatus.setText(getString(R.string.player_status_playing_file, msg.obj));
|
||||||
|
break;
|
||||||
|
case AudioPlayer.PLAYER_PLAYED_FILE:
|
||||||
|
_textPlaybackStatus.setText(getString(R.string.player_status_played_file, msg.obj));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final AdapterView.OnItemClickListener onFileClickListener = (parent, view, position, id) -> {
|
||||||
|
Object selectedItem = parent.getAdapter().getItem(position);
|
||||||
|
File selectedFile = new File(_currentDirectory, selectedItem.toString());
|
||||||
|
if (selectedFile.isDirectory()) {
|
||||||
|
loadFiles(selectedFile);
|
||||||
|
} else if (_audioPlayer == null) {
|
||||||
|
File[] files = {selectedFile};
|
||||||
|
_audioPlayer = new AudioPlayer(files, onPlayerStateChanged, this);
|
||||||
|
_audioPlayer.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final AdapterView.OnItemLongClickListener onFileLongClickListener = (parent, view, position, id) -> {
|
||||||
|
Object selectedItem = parent.getAdapter().getItem(position);
|
||||||
|
File selectedFile = new File(_currentDirectory, selectedItem.toString());
|
||||||
|
if (selectedFile.isDirectory()) {
|
||||||
|
runDeleteFromDirectoryConfirmation(selectedFile);
|
||||||
|
if (selectedFile.delete()) {
|
||||||
|
loadFiles(_currentDirectory);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runDeleteFileConfirmation(selectedFile);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
private void loadFiles(File directory) {
|
||||||
|
_dirAdapter.clear();
|
||||||
|
_currentDirectory = directory;
|
||||||
|
|
||||||
|
String title = directory.getName();
|
||||||
|
if (_root.getAbsolutePath().equals(directory.getAbsolutePath())) {
|
||||||
|
title = getString(R.string.recorder_name);
|
||||||
|
}
|
||||||
|
setTitle(title);
|
||||||
|
|
||||||
|
List<String> dirList = new ArrayList<>();
|
||||||
|
File[] fileList = directory.listFiles();
|
||||||
|
if (fileList != null) {
|
||||||
|
for (File file : fileList) {
|
||||||
|
dirList.add(file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(dirList);
|
||||||
|
|
||||||
|
for (Object dirElement: dirList) {
|
||||||
|
_dirAdapter.add(dirElement);
|
||||||
|
}
|
||||||
|
_recordingList.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteAll(File directory) {
|
||||||
|
File[] fileList = directory.listFiles();
|
||||||
|
if (fileList != null) {
|
||||||
|
for (File file : fileList) {
|
||||||
|
if (!file.delete()) {
|
||||||
|
Log.e(TAG, file.getName() + " cannot be deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadFiles(_currentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playAll() {
|
||||||
|
if (_audioPlayer == null) {
|
||||||
|
File[] files = _currentDirectory.listFiles();
|
||||||
|
_audioPlayer = new AudioPlayer(files, onPlayerStateChanged, this);
|
||||||
|
_audioPlayer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
|
||||||
|
if (!_root.getAbsolutePath().equals(_currentDirectory.getAbsolutePath())) {
|
||||||
|
_currentDirectory = _currentDirectory.getParentFile();
|
||||||
|
if (_currentDirectory != null) {
|
||||||
|
loadFiles(_currentDirectory);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runDeleteFromDirectoryConfirmation(File directory) {
|
||||||
|
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this);
|
||||||
|
alertBuilder.setMessage(getString(R.string.recorder_remove_all_confirmation_message, directory.getName()))
|
||||||
|
.setTitle(R.string.recorder_remove_all_confirmation_title)
|
||||||
|
.setPositiveButton(R.string.ok, (dialog, id) -> deleteAll(directory))
|
||||||
|
.setNegativeButton(R.string.cancel, (dialog, id) -> {})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runDeleteFileConfirmation(File file) {
|
||||||
|
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this);
|
||||||
|
alertBuilder.setMessage(getString(R.string.recorder_remove_file_confirmation_message, file.getName()))
|
||||||
|
.setTitle(R.string.recorder_remove_all_confirmation_title)
|
||||||
|
.setPositiveButton(R.string.ok, (dialog, id) -> {
|
||||||
|
if (file.delete()) {
|
||||||
|
loadFiles(_currentDirectory);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.cancel, (dialog, id) -> {})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.recorder_menu, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item)
|
||||||
|
{
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
|
||||||
|
if (itemId == R.id.recorder_play_all) {
|
||||||
|
playAll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (itemId == R.id.recorder_delete_all) {
|
||||||
|
runDeleteFromDirectoryConfirmation(_currentDirectory);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (itemId == R.id.recorder_stop) {
|
||||||
|
if (_audioPlayer != null) {
|
||||||
|
_audioPlayer.stopPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent Data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, Data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ public final class PreferenceKeys {
|
||||||
|
|
||||||
public static String CODEC2_MODE = "codec2_mode";
|
public static String CODEC2_MODE = "codec2_mode";
|
||||||
public static String CODEC2_TEST_MODE = "codec2_test_mode";
|
public static String CODEC2_TEST_MODE = "codec2_test_mode";
|
||||||
|
public static String CODEC2_VOICEMAIL = "codec2_voicemail";
|
||||||
|
|
||||||
public static String KISS_ENABLED = "kiss_enable";
|
public static String KISS_ENABLED = "kiss_enable";
|
||||||
public static String KISS_BUFFERED_ENABLED = "kiss_buffered_enable";
|
public static String KISS_BUFFERED_ENABLED = "kiss_buffered_enable";
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.radio.codec2talkie.tools;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class StorageTools {
|
||||||
|
|
||||||
|
public static boolean isExternalStorageAvailable() {
|
||||||
|
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getExternalStorage(Context context) {
|
||||||
|
File[] externalStorageVolumes =
|
||||||
|
ContextCompat.getExternalFilesDirs(context, null);
|
||||||
|
return externalStorageVolumes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getStorage(Context context) {
|
||||||
|
if (isExternalStorageAvailable()) {
|
||||||
|
return getExternalStorage(context);
|
||||||
|
} else {
|
||||||
|
return context.getFilesDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".recorder.RecorderActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textPlaybackStatus"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="status"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/listRecorder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textPlaybackStatus" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,9 +1,18 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item
|
<group android:id="@+id/group_main">
|
||||||
android:id="@+id/preferences"
|
<item
|
||||||
android:title="@string/menu_preferences"></item>
|
android:id="@+id/voicemail"
|
||||||
<item
|
android:title="@string/menu_recorder"></item>
|
||||||
android:id="@+id/exit"
|
</group>
|
||||||
android:title="@string/menu_exit"></item>
|
<group android:id="@+id/group_settings">
|
||||||
|
<item
|
||||||
|
android:id="@+id/preferences"
|
||||||
|
android:title="@string/menu_settings"></item>
|
||||||
|
</group>
|
||||||
|
<group android:id="@+id/group_exit">
|
||||||
|
<item
|
||||||
|
android:id="@+id/exit"
|
||||||
|
android:title="@string/menu_exit"></item>
|
||||||
|
</group>
|
||||||
</menu>
|
</menu>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/recorder_stop"
|
||||||
|
android:title="@string/recorder_menu_stop"></item>
|
||||||
|
<item
|
||||||
|
android:id="@+id/recorder_play_all"
|
||||||
|
android:title="@string/recorder_menu_play_all"></item>
|
||||||
|
<item
|
||||||
|
android:id="@+id/recorder_delete_all"
|
||||||
|
android:title="@string/recorder_menu_delete_all"></item>
|
||||||
|
</menu>
|
|
@ -2,7 +2,7 @@
|
||||||
<string name="app_name">Codec2 Talkie</string>
|
<string name="app_name">Codec2 Talkie</string>
|
||||||
<string name="push_to_talk">PUSH TO TALK</string>
|
<string name="push_to_talk">PUSH TO TALK</string>
|
||||||
|
|
||||||
<string name="menu_preferences">Preferences</string>
|
<string name="menu_settings">Settings</string>
|
||||||
<string name="menu_exit">Exit</string>
|
<string name="menu_exit">Exit</string>
|
||||||
|
|
||||||
<string name="ports_category_title">TNC Settings</string>
|
<string name="ports_category_title">TNC Settings</string>
|
||||||
|
@ -24,8 +24,10 @@
|
||||||
|
|
||||||
<string name="codec2_category_title">Codec2 Settings</string>
|
<string name="codec2_category_title">Codec2 Settings</string>
|
||||||
<string name="codec2_mode_title">Mode/Speed</string>
|
<string name="codec2_mode_title">Mode/Speed</string>
|
||||||
<string name="codec2_test_mode_title">Record/Playback Test Mode</string>
|
<string name="codec2_test_mode_title">Echo Test Mode</string>
|
||||||
<string name="codec2_test_mode_summary">Enables own voice recording/playback without transmission</string>
|
<string name="codec2_test_mode_summary">Records and plays recording without transmission</string>
|
||||||
|
<string name="codec2_recorder_title">Enable Recorder</string>
|
||||||
|
<string name="codec2_recorder_summary">Record incoming and outgoing transmissions for future playback</string>
|
||||||
<string-array name="codec2_modes">
|
<string-array name="codec2_modes">
|
||||||
<item>MODE_450=10</item>
|
<item>MODE_450=10</item>
|
||||||
<item>MODE_700C=8</item>
|
<item>MODE_700C=8</item>
|
||||||
|
@ -52,7 +54,7 @@
|
||||||
<string name="kiss_basic_summary">Set CSMA and TX delay/tail parameters</string>
|
<string name="kiss_basic_summary">Set CSMA and TX delay/tail parameters</string>
|
||||||
|
|
||||||
<string name="kiss_basic_p_title">CSMA persistence P</string>
|
<string name="kiss_basic_p_title">CSMA persistence P</string>
|
||||||
<string name="kiss_basic_p_summary">Set CSMA persistence P (1-255)</string>
|
<string name="kiss_basic_p_summary">Set CSMA persistence P (1–255)</string>
|
||||||
|
|
||||||
<string name="kiss_basic_slot_time_title">CSMA slot time</string>
|
<string name="kiss_basic_slot_time_title">CSMA slot time</string>
|
||||||
<string name="kiss_basic_slot_time_summary">Set CSMA slot time (milliseconds / 10)</string>
|
<string name="kiss_basic_slot_time_summary">Set CSMA slot time (milliseconds / 10)</string>
|
||||||
|
@ -103,5 +105,21 @@
|
||||||
|
|
||||||
<string name="app_keep_screen_on_title">Keep screen ON</string>
|
<string name="app_keep_screen_on_title">Keep screen ON</string>
|
||||||
<string name="app_keep_screen_on_summary">Prevent screen switching off when app is active</string>
|
<string name="app_keep_screen_on_summary">Prevent screen switching off when app is active</string>
|
||||||
|
<string name="menu_recorder">Play Recordings</string>
|
||||||
|
<string name="recorder_menu_play_all">Play All</string>
|
||||||
|
<string name="recorder_menu_delete_all">Delete All</string>
|
||||||
|
<string name="recorder_remove_all_confirmation_message">Remove all recordings from %1$s?</string>
|
||||||
|
<string name="recorder_remove_all_confirmation_title">Remove confirmation</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="recorder_name">Recorder</string>
|
||||||
|
<string name="recorder_status_label">REC</string>
|
||||||
|
<string name="player_status_stopped">Playback stopped</string>
|
||||||
|
<string name="player_status_started">Playback started</string>
|
||||||
|
<string name="player_status_error">Playback error, %1$s</string>
|
||||||
|
<string name="player_status_playing_file">Playing file: %1$s</string>
|
||||||
|
<string name="player_status_played_file">Played file: %1$s</string>
|
||||||
|
<string name="recorder_menu_stop">Stop</string>
|
||||||
|
<string name="recorder_remove_file_confirmation_message">Remove recording %1$s?</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -40,6 +40,13 @@
|
||||||
app:defaultValue="false">
|
app:defaultValue="false">
|
||||||
</SwitchPreference>
|
</SwitchPreference>
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
app:key="codec2_voicemail"
|
||||||
|
app:title="@string/codec2_recorder_title"
|
||||||
|
app:summary="@string/codec2_recorder_summary"
|
||||||
|
app:defaultValue="false">
|
||||||
|
</SwitchPreference>
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
|
|
Ładowanie…
Reference in New Issue