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
|
||||
- **Codec2**
|
||||
- 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**
|
||||
- Change default baud rate for USB port
|
||||
- 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 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)
|
||||
- **Recording player**, simple player, which allows TX/RX recording playback and removal
|
||||
|
||||
# Suitable radios and modems
|
||||
- Tested, works:
|
||||
|
|
|
@ -11,7 +11,7 @@ android {
|
|||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.3"
|
||||
versionName "1.4"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
<activity
|
||||
android:name=".settings.BluetoothSettingsActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".recorder.RecorderActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize">
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package com.radio.codec2talkie;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.MenuCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import android.Manifest;
|
||||
|
@ -23,7 +25,6 @@ import android.os.Handler;
|
|||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -36,9 +37,11 @@ import android.widget.ProgressBar;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.radio.codec2talkie.audio.AudioProcessor;
|
||||
import com.radio.codec2talkie.connect.BluetoothConnectActivity;
|
||||
import com.radio.codec2talkie.connect.SocketHandler;
|
||||
import com.radio.codec2talkie.protocol.ProtocolFactory;
|
||||
import com.radio.codec2talkie.recorder.RecorderActivity;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.settings.SettingsActivity;
|
||||
import com.radio.codec2talkie.tools.RadioTools;
|
||||
|
@ -53,12 +56,11 @@ import java.util.Locale;
|
|||
|
||||
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_USB = 2;
|
||||
private final static int REQUEST_PERMISSIONS = 3;
|
||||
private final static int REQUEST_SETTINGS = 4;
|
||||
private final static int REQUEST_VOICEMAIL = 5;
|
||||
|
||||
// S9 level at -93 dBm as per VHF Managers Handbook
|
||||
private final static int S_METER_S0_VALUE_DB = -153;
|
||||
|
@ -90,6 +92,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String appName = getResources().getString(R.string.app_name);
|
||||
|
@ -166,8 +169,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
startActivityForResult(bluetoothConnectIntent, REQUEST_CONNECT_BT);
|
||||
}
|
||||
|
||||
protected void startVoicemailActivity() {
|
||||
Intent voicemailIntent = new Intent(this, RecorderActivity.class);
|
||||
startActivityForResult(voicemailIntent, REQUEST_VOICEMAIL);
|
||||
}
|
||||
|
||||
protected boolean requestPermissions() {
|
||||
List<String> permissionsToRequest = new LinkedList<String>();
|
||||
List<String> permissionsToRequest = new LinkedList<>();
|
||||
|
||||
for (String permission : _requiredPermissions) {
|
||||
if (ContextCompat.checkSelfPermission(MainActivity.this, permission) == PackageManager.PERMISSION_DENIED) {
|
||||
|
@ -215,6 +223,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuCompat.setGroupDividerEnabled(menu, true);
|
||||
getMenuInflater().inflate(R.menu.main_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
@ -229,6 +238,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
startActivityForResult(settingsIntent, REQUEST_SETTINGS);
|
||||
return true;
|
||||
}
|
||||
if (itemId == R.id.voicemail) {
|
||||
startVoicemailActivity();
|
||||
return true;
|
||||
}
|
||||
else if (itemId == R.id.exit) {
|
||||
stopRunning();
|
||||
return true;
|
||||
|
@ -303,7 +316,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
};
|
||||
|
||||
@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);
|
||||
if (requestCode == REQUEST_PERMISSIONS) {
|
||||
boolean allGranted = true;
|
||||
|
@ -427,9 +440,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
speedModeInfo += ", " + protocolType.toString();
|
||||
|
||||
boolean voicemailEnabled = _sharedPreferences.getBoolean(PreferenceKeys.CODEC2_VOICEMAIL, false);
|
||||
|
||||
if (voicemailEnabled) {
|
||||
speedModeInfo += ", " + getString(R.string.recorder_status_label);
|
||||
}
|
||||
_textCodecMode.setText(speedModeInfo);
|
||||
|
||||
_audioProcessor = new AudioProcessor(transportType, protocolType, codec2ModeId, onAudioProcessorStateChanged, getApplicationContext());
|
||||
_audioProcessor = new AudioProcessor(transportType, protocolType, voicemailEnabled, codec2ModeId, onAudioProcessorStateChanged, getApplicationContext());
|
||||
_audioProcessor.start();
|
||||
} catch (IOException e) {
|
||||
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.media.AudioAttributes;
|
||||
|
@ -11,12 +11,8 @@ import android.os.Looper;
|
|||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Timer;
|
||||
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_QUIT = 12;
|
||||
|
||||
private static int AUDIO_MIN_LEVEL = -70;
|
||||
private static int AUDIO_MAX_LEVEL = 0;
|
||||
private static final int AUDIO_MIN_LEVEL = -70;
|
||||
private static final int AUDIO_MAX_LEVEL = 0;
|
||||
private final int AUDIO_SAMPLE_SIZE = 8000;
|
||||
|
||||
private final int PROCESS_INTERVAL_MS = 20;
|
||||
|
@ -86,13 +82,14 @@ public class AudioProcessor extends Thread {
|
|||
private final Context _context;
|
||||
|
||||
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;
|
||||
|
||||
_context = context;
|
||||
|
||||
_transport = TransportFactory.create(transportType);
|
||||
_protocol = ProtocolFactory.create(protocolType);
|
||||
_protocol = ProtocolFactory.create(protocolType, codec2Mode, voicemailEnabled);
|
||||
|
||||
_processPeriodicTimer = new Timer();
|
||||
|
||||
|
@ -340,8 +337,6 @@ public class AudioProcessor extends Thread {
|
|||
// playback
|
||||
if (_protocol.receive(_protocolReceiveCallback)) {
|
||||
sendStatusUpdate(PROCESSOR_RECEIVING);
|
||||
} else {
|
||||
// idling
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package com.radio.codec2talkie.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class Callback {
|
||||
abstract protected void onReceiveAudioFrames(byte [] frame);
|
||||
abstract protected void onReceiveSignalLevel(byte [] rawData);
|
||||
|
|
|
@ -8,7 +8,7 @@ public class ProtocolFactory {
|
|||
KISS_BUFFERED("KISS BUFFERED"),
|
||||
KISS_PARROT("KISS PARROT");
|
||||
|
||||
private String _name;
|
||||
private final String _name;
|
||||
|
||||
ProtocolType(String 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) {
|
||||
case KISS:
|
||||
return new Kiss();
|
||||
proto = new Kiss();
|
||||
break;
|
||||
case KISS_BUFFERED:
|
||||
return new KissBuffered();
|
||||
proto = new KissBuffered();
|
||||
break;
|
||||
case KISS_PARROT:
|
||||
return new KissParrot();
|
||||
proto = new KissParrot();
|
||||
break;
|
||||
case RAW:
|
||||
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_TEST_MODE = "codec2_test_mode";
|
||||
public static String CODEC2_VOICEMAIL = "codec2_voicemail";
|
||||
|
||||
public static String KISS_ENABLED = "kiss_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"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/preferences"
|
||||
android:title="@string/menu_preferences"></item>
|
||||
<item
|
||||
android:id="@+id/exit"
|
||||
android:title="@string/menu_exit"></item>
|
||||
<group android:id="@+id/group_main">
|
||||
<item
|
||||
android:id="@+id/voicemail"
|
||||
android:title="@string/menu_recorder"></item>
|
||||
</group>
|
||||
<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>
|
|
@ -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="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="ports_category_title">TNC Settings</string>
|
||||
|
@ -24,8 +24,10 @@
|
|||
|
||||
<string name="codec2_category_title">Codec2 Settings</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_summary">Enables own voice recording/playback without transmission</string>
|
||||
<string name="codec2_test_mode_title">Echo Test Mode</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">
|
||||
<item>MODE_450=10</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_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_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_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>
|
|
@ -40,6 +40,13 @@
|
|||
app:defaultValue="false">
|
||||
</SwitchPreference>
|
||||
|
||||
<SwitchPreference
|
||||
app:key="codec2_voicemail"
|
||||
app:title="@string/codec2_recorder_title"
|
||||
app:summary="@string/codec2_recorder_summary"
|
||||
app:defaultValue="false">
|
||||
</SwitchPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
|
|
Ładowanie…
Reference in New Issue