Merge pull request #12 from sh123/voicemail

Implement basic recorder
pull/14/head 1.4
sh123 2021-10-10 17:25:22 +03:00 zatwierdzone przez GitHub
commit ea2655515a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
17 zmienionych plików z 708 dodań i 37 usunięć

Wyświetl plik

@ -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:

Wyświetl plik

@ -11,7 +11,7 @@ android {
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.3"
versionName "1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

Wyświetl plik

@ -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">

Wyświetl plik

@ -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();

Wyświetl plik

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

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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) {}
}
}

Wyświetl plik

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

Wyświetl plik

@ -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";

Wyświetl plik

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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 (1255)</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>

Wyświetl plik

@ -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