Java tutorial
package com.mantz_it.rfanalyzer.ui.activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; import com.mantz_it.rfanalyzer.AnalyzerProcessingLoop; import com.mantz_it.rfanalyzer.BookmarksDialog; import com.mantz_it.rfanalyzer.Demodulator; import com.mantz_it.rfanalyzer.IQSource; import com.mantz_it.rfanalyzer.R; import com.mantz_it.rfanalyzer.RFControlInterface; import com.mantz_it.rfanalyzer.Scheduler; import com.mantz_it.rfanalyzer.device.file.FileIQSource; import com.mantz_it.rfanalyzer.device.hackrf.HackrfSource; import com.mantz_it.rfanalyzer.device.hiqsdr.HiqsdrSource; import com.mantz_it.rfanalyzer.device.rtlsdr.RtlsdrSource; import com.mantz_it.rfanalyzer.sdr.controls.RXFrequency; import com.mantz_it.rfanalyzer.sdr.controls.RXSampleRate; import com.mantz_it.rfanalyzer.ui.component.AnalyzerSurface; import com.mantz_it.rfanalyzer.ui.util.Toaster; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * <h1>RF Analyzer - Main Activity</h1> * <p/> * Module: MainActivity.java * Description: Main Activity of the RF Analyzer * * @author Dennis Mantz * <p/> * Copyright (C) 2014 Dennis Mantz * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher * <p/> * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * <p/> * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * <p/> * You should have received a copy of the GNU General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ public class MainActivity extends AppCompatActivity implements IQSource.Callback, RFControlInterface { private MenuItem mi_startStop = null; private MenuItem mi_demodulationMode = null; private MenuItem mi_record = null; private FrameLayout fl_analyzerFrame = null; private AnalyzerSurface analyzerSurface = null; private AnalyzerProcessingLoop analyzerProcessingLoop = null; private Scheduler scheduler = null; private Demodulator demodulator = null; private SharedPreferences preferences = null; private Bundle savedInstanceState = null; private Process logcat = null; private String versionName = null; private boolean running = false; private File recordingFile = null; private int demodulationMode = Demodulator.DEMODULATION_OFF; private IQSource source = null; private RXFrequency rxFrequency; private RXSampleRate rxSampleRate; private Toaster toaster; private static final String LOGTAG = "MainActivity"; private static final String RECORDING_DIR = "RFAnalyzer"; public static final int RTL2832U_RESULT_CODE = 1234; // arbitrary value, used when sending intent to RTL2832U /** * arbitrary value, used when requesting permission to open file for the file source */ public static final int PERMISSION_REQUEST_FILE_SOURCE_READ_FILES = 1111; /** * arbitrary value, used when requesting permission to write file for the recording feature */ public static final int PERMISSION_REQUEST_RECORDING_WRITE_FILES = 1112; private static final int FILE_SOURCE = 0; private static final int HACKRF_SOURCE = 1; private static final int RTLSDR_SOURCE = 2; private static final int HIQSDR_SOURCE = 3; private static final String[] SOURCE_NAMES = new String[] { "filesource", "hackrf", "rtlsdr", "hiqsdr" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); toaster = new Toaster(this); setContentView(R.layout.activity_main); this.savedInstanceState = savedInstanceState; // Set default Settings on first run: PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // Get reference to the shared preferences: preferences = PreferenceManager.getDefaultSharedPreferences(this); // todo: separate preferences files for different PreferenceUsers // Overwrite defaults for file paths in the preferences: String extStorage = Environment.getExternalStorageDirectory().getAbsolutePath(); // get the path to the ext. storage // File Source file: String defaultFile = getString(R.string.pref_filesource_file_default); if (preferences.getString(getString(R.string.pref_filesource_file), "").equals(defaultFile)) preferences.edit().putString(getString(R.string.pref_filesource_file), extStorage + "/" + defaultFile) .apply(); // Log file: defaultFile = getString(R.string.pref_logfile_default); if (preferences.getString(getString(R.string.pref_logfile), "").equals(defaultFile)) preferences.edit().putString(getString(R.string.pref_logfile), extStorage + "/" + defaultFile).apply(); // Start logging if enabled: if (preferences.getBoolean(getString(R.string.pref_logging), false)) { if (ContextCompat.checkSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE") == PackageManager.PERMISSION_GRANTED) { try { File logfile = new File(preferences.getString(getString(R.string.pref_logfile), "")); logfile.getParentFile().mkdir(); // Create folder logcat = Runtime.getRuntime().exec("logcat -f " + logfile); Log.i("MainActivity", "onCreate: started logcat (" + logcat.toString() + ") to " + logfile); } catch (Exception e) { Log.e("MainActivity", "onCreate: Failed to start logging!"); } } else { preferences.edit().putBoolean(getString(R.string.pref_logging), false).apply(); Log.i(LOGTAG, "onCreate: deactivate logging because of missing storage permission."); } } // Get version name: try { versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; Log.i(LOGTAG, "This is RF Analyzer " + versionName + " by Dennis Mantz (modified)."); } catch (PackageManager.NameNotFoundException e) { Log.e(LOGTAG, "onCreate: Cannot read version name: " + e.getMessage()); } // Get references to the GUI components: fl_analyzerFrame = (FrameLayout) findViewById(R.id.fl_analyzerFrame); // Create a analyzer surface: analyzerSurface = new AnalyzerSurface(this, this); analyzerSurface.init(preferences); // Put the analyzer surface in the analyzer frame of the layout: fl_analyzerFrame.addView(analyzerSurface); // Restore / Initialize the running state and the demodulator mode: if (savedInstanceState != null) { running = savedInstanceState.getBoolean(getString(R.string.save_state_running)); demodulationMode = savedInstanceState.getInt(getString(R.string.save_state_demodulatorMode)); /* BUGFIX / WORKAROUND: * The RTL2832U driver will not allow to close the socket and immediately start the driver * again to reconnect after an orientation change / app kill + restart. * It will report back in onActivityResult() with a -1 (not specified). * * Work-around: * 1) We won't restart the Analyzer if the current source is set to a local RTL-SDR instance: * 2) Delay the restart of the Analyzer after the driver was shut down correctly... */ if (running && Integer.parseInt( preferences.getString(getString(R.string.pref_sourceType), "1")) == RTLSDR_SOURCE && !preferences.getBoolean(getString(R.string.pref_rtlsdr_externalServer), false)) { // 1) don't start Analyzer immediately running = false; // Just inform the user about what is going on (why does this take so long? ...) toaster.showShort("Stopping and restarting RTL2832U driver..."); // 2) Delayed start of the Analyzer: // todo: can we use notifyAll() instead of this? Thread timer = new Thread(() -> { try { Thread.sleep(1500); startAnalyzer(); } catch (InterruptedException e) { Log.e(LOGTAG, "onCreate: (timer thread): Interrupted while sleeping."); } }, "Timer Thread"); timer.start(); } } else { // Set running to true if autostart is enabled (this will start the analyzer in onStart() ) running = preferences.getBoolean((getString(R.string.pref_autostart)), false); } // Set the hardware volume keys to work on the music audio stream: setVolumeControlStream(AudioManager.STREAM_MUSIC); } @Override protected void onDestroy() { super.onDestroy(); // close source if (source != null && source.isOpen()) source.close(); // stop logging: if (logcat != null) { try { logcat.destroy(); logcat.waitFor(); Log.i(LOGTAG, "onDestroy: logcat exit value: " + logcat.exitValue()); } catch (Exception e) { Log.e(LOGTAG, "onDestroy: couldn't stop logcat: " + e.getMessage()); } } // shut down RTL2832U driver if running: if (running && Integer .parseInt(preferences.getString(getString(R.string.pref_sourceType), "1")) == RTLSDR_SOURCE && !preferences.getBoolean(getString(R.string.pref_rtlsdr_externalServer), false)) { try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClassName("marto.rtl_tcp_andro", "com.sdrtouch.rtlsdr.DeviceOpenActivity"); intent.setData(Uri.parse("iqsrc://-x")); // -x is invalid. will cause the driver to shut down (if running) startActivity(intent); } catch (ActivityNotFoundException e) { Log.e(LOGTAG, "onDestroy: RTL2832U is not installed"); } } } // todo: inverse to analyzerSurface.saveState(Bundle) @Override protected void onSaveInstanceState(Bundle outState) { outState.putBoolean(getString(R.string.save_state_running), running); outState.putInt(getString(R.string.save_state_demodulatorMode), demodulationMode); // todo: also save source settings? definitely in need of interface handling settings if (analyzerSurface != null) { outState.putLong(getString(R.string.save_state_channelFrequency), analyzerSurface.getChannelFrequency()); outState.putInt(getString(R.string.save_state_channelWidth), analyzerSurface.getChannelWidth()); outState.putFloat(getString(R.string.save_state_squelch), analyzerSurface.getSquelch()); outState.putLong(getString(R.string.save_state_virtualFrequency), analyzerSurface.getVirtualFrequency()); outState.putInt(getString(R.string.save_state_virtualSampleRate), analyzerSurface.getVirtualSampleRate()); outState.putFloat(getString(R.string.save_state_minDB), analyzerSurface.getMinDB()); outState.putFloat(getString(R.string.save_state_maxDB), analyzerSurface.getMaxDB()); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); // Get a reference to the start-stop button: mi_startStop = menu.findItem(R.id.action_startStop); mi_demodulationMode = menu.findItem(R.id.action_setDemodulation); mi_record = menu.findItem(R.id.action_record); // update the action bar icons and titles according to the app state: updateActionBar(); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); switch (id) { case R.id.action_startStop: if (running) stopAnalyzer(); else startAnalyzer(); break; case R.id.action_setDemodulation: showDemodulationDialog(); break; case R.id.action_setFrequency: tuneToFrequency(); break; case R.id.action_setGain: adjustGain(); break; case R.id.action_autoscale: analyzerSurface.autoscale(); break; case R.id.action_record: if (scheduler != null && scheduler.isRecording()) stopRecording(); else showRecordingDialog(); break; case R.id.action_bookmarks: showBookmarksDialog(); break; case R.id.action_settings: Intent intentShowSettings = new Intent(getApplicationContext(), SettingsActivity.class); startActivity(intentShowSettings); break; case R.id.action_help: Intent intentShowHelp = new Intent(Intent.ACTION_VIEW); intentShowHelp.setData(Uri.parse(getString(R.string.help_url))); startActivity(intentShowHelp); break; case R.id.action_info: showInfoDialog(); break; default: } return true; } /** * Will update the action bar icons and titles according to the current app state */ private void updateActionBar() { this.runOnUiThread(() -> { // Set title and icon of the start/stop button according to the state: if (mi_startStop != null) { if (running) { mi_startStop.setTitle(R.string.action_stop); mi_startStop.setIcon(R.drawable.ic_action_pause); } else { mi_startStop.setTitle(R.string.action_start); mi_startStop.setIcon(R.drawable.ic_action_play); } } // Set title and icon for the demodulator mode button if (mi_demodulationMode != null) { int iconRes; int titleRes; switch (demodulationMode) { case Demodulator.DEMODULATION_OFF: iconRes = R.drawable.ic_action_demod_off; titleRes = R.string.action_demodulation_off; break; case Demodulator.DEMODULATION_AM: iconRes = R.drawable.ic_action_demod_am; titleRes = R.string.action_demodulation_am; break; case Demodulator.DEMODULATION_NFM: iconRes = R.drawable.ic_action_demod_nfm; titleRes = R.string.action_demodulation_nfm; break; case Demodulator.DEMODULATION_WFM: iconRes = R.drawable.ic_action_demod_wfm; titleRes = R.string.action_demodulation_wfm; break; case Demodulator.DEMODULATION_LSB: iconRes = R.drawable.ic_action_demod_lsb; titleRes = R.string.action_demodulation_lsb; break; case Demodulator.DEMODULATION_USB: iconRes = R.drawable.ic_action_demod_usb; titleRes = R.string.action_demodulation_usb; break; default: Log.e(LOGTAG, "updateActionBar: invalid mode: " + demodulationMode); iconRes = -1; titleRes = -1; break; } if (titleRes > 0 && iconRes > 0) { mi_demodulationMode.setTitle(titleRes); mi_demodulationMode.setIcon(iconRes); } } // Set title and icon of the record button according to the state: if (mi_record != null) { if (recordingFile != null) { mi_record.setTitle(R.string.action_recordOn); mi_record.setIcon(R.drawable.ic_action_record_on); } else { mi_record.setTitle(R.string.action_recordOff); mi_record.setIcon(R.drawable.ic_action_record_off); } } }); } @Override protected void onStart() { super.onStart(); // Check if the user changed the preferences: checkForChangedPreferences(); // Start the analyzer if running is true: if (running) startAnalyzer(); // on the first time after the app was killed by the system, savedInstanceState will be // non-null and we restore the settings: if (savedInstanceState != null) { restoreAnalyzerSurface(analyzerSurface, savedInstanceState); if (demodulator != null && scheduler != null) { demodulator.setChannelWidth(savedInstanceState.getInt(getString(R.string.save_state_channelWidth))); scheduler.setChannelFrequency( savedInstanceState.getLong(getString(R.string.save_state_channelFrequency))); } savedInstanceState = null; // not needed any more... } } private void restoreAnalyzerSurface(AnalyzerSurface analyzerSurface, Bundle savedInstanceState) { analyzerSurface .setVirtualFrequency(savedInstanceState.getLong(getString(R.string.save_state_virtualFrequency))); analyzerSurface .setVirtualSampleRate(savedInstanceState.getInt(getString(R.string.save_state_virtualSampleRate))); analyzerSurface.setDBScale(savedInstanceState.getFloat(getString(R.string.save_state_minDB)), savedInstanceState.getFloat(getString(R.string.save_state_maxDB))); analyzerSurface .setChannelFrequency(savedInstanceState.getLong(getString(R.string.save_state_channelFrequency))); analyzerSurface.setChannelWidth(savedInstanceState.getInt(getString(R.string.save_state_channelWidth))); analyzerSurface.setSquelch(savedInstanceState.getFloat(getString(R.string.save_state_squelch))); } // todo: inverse to source.storeState(SharedPreferences.Editor) @Override protected void onStop() { super.onStop(); boolean runningSaved = running; // save the running state, to restore it after the app re-starts... stopAnalyzer(); // will stop the processing loop, scheduler and source running = runningSaved; // running will be saved in onSaveInstanceState() // safe preferences: if (source != null) { SharedPreferences.Editor edit = preferences.edit(); if (source instanceof HackrfSource || source instanceof RtlsdrSource) { edit.putLong(getString(R.string.pref_frequency), rxFrequency.get()); edit.putInt(getString(R.string.pref_sampleRate), rxSampleRate.get()); } else { //todo: method to save settings by source edit.putString(getString(R.string.pref_hiqsdr_rx_frequency), Long.toString(rxFrequency.get())); edit.putString(getString(R.string.pref_hiqsdr_sampleRate), Integer.toString(rxSampleRate.get())); } edit.apply(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // err_info from RTL2832U: String[] rtlsdrErrInfo = { "permission_denied", "root_required", "no_devices_found", "unknown_error", "replug", "already_running" }; switch (requestCode) { case RTL2832U_RESULT_CODE: // This happens if the RTL2832U driver was started. // We check for errors and print them: if (resultCode == RESULT_OK) Log.i(LOGTAG, "onActivityResult: RTL2832U driver was successfully started."); else { int errorId = -1; int exceptionCode = 0; String detailedDescription = null; if (data != null) { errorId = data.getIntExtra("marto.rtl_tcp_andro.RtlTcpExceptionId", -1); exceptionCode = data.getIntExtra("detailed_exception_code", 0); detailedDescription = data.getStringExtra("detailed_exception_message"); } String errorMsg = "ERROR NOT SPECIFIED"; if (errorId >= 0 && errorId < rtlsdrErrInfo.length) errorMsg = rtlsdrErrInfo[errorId]; Log.e(LOGTAG, "onActivityResult: RTL2832U driver returned with error: " + errorMsg + " (" + errorId + ")" + (detailedDescription != null ? ": " + detailedDescription + " (" + exceptionCode + ")" : "")); if (source != null && source instanceof RtlsdrSource) { toaster.showLong( "Error with Source [" + source.getName() + "]: " + errorMsg + " (" + errorId + ")" + (detailedDescription != null ? ": " + detailedDescription + " (" + exceptionCode + ")" : "")); source.close(); } } break; } } //@Override public void onRequestPermissionsResult(int requestCode, String permissions[], @NonNull int[] grantResults) { switch (requestCode) { case PERMISSION_REQUEST_FILE_SOURCE_READ_FILES: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (source != null && source instanceof FileIQSource) { if (!source.open(this, this)) Log.e(LOGTAG, "onRequestPermissionResult: source.open() exited with an error."); } else { Log.e(LOGTAG, "onRequestPermissionResult: source is null or of other type."); } } } break; case PERMISSION_REQUEST_RECORDING_WRITE_FILES: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { showRecordingDialog(); } } break; } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } @Override public void onIQSourceReady(IQSource source) { // is called after source.open() Log.i(LOGTAG, "onIQSourceReady: " + source.getName()); if (running) startAnalyzer(); // will start the processing loop, scheduler and source } @Override public void onIQSourceError(final IQSource source, final String message) { Log.e(LOGTAG, source.getName() + ": " + message); this.runOnUiThread(() -> toaster.showLong("Error with Source [" + source.getName() + "]: " + message)); stopAnalyzer(); if (this.source != null && this.source.isOpen()) this.source.close(); } /** * Reflection-based source settings updater * * @param clazz desired class of source * @return current source with updated settings, or new source if current source type isn't instance of desired source. */ protected void updateSourcePreferences(final Class<?> clazz) { // if src is of desired class -- just update if (clazz.isInstance(this.source)) { setSource(this.source.updatePreferences(this, preferences)); analyzerSurface.setSource(this.source); } else { // create new this.source.close(); final String msg; try { // we can't force sources to implement constructor with needed parameters, // to drop need of tracking all sources that could be added later just use reflection // to call constructor with current Context and SharedPreferences and let source configure itself Constructor ctor = clazz.getDeclaredConstructor(Context.class, SharedPreferences.class); ctor.setAccessible(true); setSource((IQSource) ctor.newInstance(this, preferences)); analyzerSurface.setSource(this.source); return; } catch (NoSuchMethodException e) { Log.e(LOGTAG, "updateSourcePreferences: " + (msg = "selected source doesn't have constructor with demanded parameters (Context, SharedPreferences)")); e.printStackTrace(); } catch (IllegalAccessException e) { Log.e(LOGTAG, "updateSourcePreferences: " + (msg = "selected source doesn't have accessible constructor with demanded parameters (Context, SharedPreferences)")); e.printStackTrace(); } catch (InstantiationException e) { Log.e(LOGTAG, "updateSourcePreferences: " + (msg = "selected source doesn't have accessible for MainActivity constructor with demanded parameters (Context, SharedPreferences)")); e.printStackTrace(); } catch (InvocationTargetException e) { Log.e(LOGTAG, "updateSourcePreferences: " + (msg = "source's constructor thrown exception: " + e.getMessage())); e.printStackTrace(); } stopAnalyzer(); this.runOnUiThread( () -> toaster.showLong("Error with instantiating source [" + clazz.getName() + "]: ")); setSource(null); } } /** * Will check if any preference conflicts with the current state of the app and fix it */ public void checkForChangedPreferences() { int sourceType = Integer.parseInt(preferences.getString(getString(R.string.pref_sourceType), "1")); /* todo: rework settings repository, so we could use reflection to instantiate source instead of hardcoded switch */ /* todo dependency injection*/ if (source != null) { switch (sourceType) { case FILE_SOURCE: updateSourcePreferences(FileIQSource.class); break; case HACKRF_SOURCE: updateSourcePreferences(HackrfSource.class); break; case RTLSDR_SOURCE: updateSourcePreferences(RtlsdrSource.class); break; case HIQSDR_SOURCE: updateSourcePreferences(HiqsdrSource.class); break; default: Log.e(LOGTAG, "checkForChangedPreferences: selected source type (" + sourceType + "is not supported"); } } if (analyzerSurface != null) { onPreferencesChanged(analyzerSurface, preferences); } // Screen Orientation: String screenOrientation = preferences.getString(getString(R.string.pref_screenOrientation), "auto") .toLowerCase(); int orientation; switch (screenOrientation) { case "landscape": orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; break; case "portrait": orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; break; case "reverse_landscape": orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; break; case "reverse_portrait": orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; break; default: case "auto": orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; break; } setRequestedOrientation(orientation); } public void onPreferencesChanged(AnalyzerSurface analyzerSurface, SharedPreferences preferences) { // todo: move this to AnalyzerSurface, create separate interface for updating settings? // All GUI settings will just be overwritten: analyzerSurface.setVerticalScrollEnabled(preferences.getBoolean(getString(R.string.pref_scrollDB), true)); analyzerSurface.setVerticalZoomEnabled(preferences.getBoolean(getString(R.string.pref_zoomDB), true)); analyzerSurface.setDecoupledAxis(preferences.getBoolean(getString(R.string.pref_decoupledAxis), false)); analyzerSurface.setDisplayRelativeFrequencies( preferences.getBoolean(getString(R.string.pref_relativeFrequencies), false)); analyzerSurface.setWaterfallColorMapType( Integer.parseInt(preferences.getString(getString(R.string.pref_colorMapType), "4"))); analyzerSurface.setFftDrawingType( Integer.parseInt(preferences.getString(getString(R.string.pref_fftDrawingType), "2"))); analyzerSurface .setAverageLength(Integer.parseInt(preferences.getString(getString(R.string.pref_averaging), "0"))); analyzerSurface.setPeakHoldEnabled(preferences.getBoolean(getString(R.string.pref_peakHold), false)); analyzerSurface.setFftRatio( Float.parseFloat(preferences.getString(getString(R.string.pref_spectrumWaterfallRatio), "0.5"))); analyzerSurface .setFontSize(Integer.parseInt(preferences.getString(getString(R.string.pref_fontSize), "2"))); analyzerSurface.setShowDebugInformation( preferences.getBoolean(getString(R.string.pref_showDebugInformation), false)); } public void setSource(IQSource source) { //if (Proxy.isProxyClass(source.getClass())) this.source = source; //else this.source = MethodInterceptor.wrapWithLog(source, "IQSource", IQSource.class); this.rxFrequency = source.getControl(RXFrequency.class); this.rxSampleRate = source.getControl(RXSampleRate.class); } /** * Will create a IQ Source instance according to the user settings. * * @return true on success; false on error */ public boolean createSource() { int sourceType = Integer.parseInt(preferences.getString(getString(R.string.pref_sourceType), "1")); // todo: rework settings repository to use reflection instead of hardcoded switch statement switch (sourceType) { case FILE_SOURCE: setSource(new FileIQSource(this, preferences)); break; case HACKRF_SOURCE: setSource(new HackrfSource(this, preferences)); break; case RTLSDR_SOURCE: setSource(new RtlsdrSource(this, preferences)); break; case HIQSDR_SOURCE: setSource(new HiqsdrSource(this, preferences)); break; default: Log.e(LOGTAG, "createSource: Invalid source type: " + sourceType); return false; } // inform the analyzer surface about the new source analyzerSurface.setSource(source); return true; } /** * Will open the IQ Source instance. * Note: some sources need special treatment on opening, like the rtl-sdr source. * * @return true on success; false on error */ public boolean openSource() { int sourceType = Integer.parseInt(preferences.getString(getString(R.string.pref_sourceType), "1")); switch (sourceType) { case FILE_SOURCE: if (source != null && source instanceof FileIQSource) { // Check for the READ_EXTERNAL_STORAGE permission: if (ContextCompat.checkSelfPermission(this, "android.permission.READ_EXTERNAL_STORAGE") != PackageManager.PERMISSION_GRANTED) { // request permission: ActivityCompat.requestPermissions(this, new String[] { "android.permission.READ_EXTERNAL_STORAGE" }, PERMISSION_REQUEST_FILE_SOURCE_READ_FILES); return true; // return and wait for the response (is handled in onRequestPermissionResult()) } else { return source.open(this, this); } } else { Log.e(LOGTAG, "openSource: sourceType is FILE_SOURCE, but source is null or of other type."); return false; } case HACKRF_SOURCE: if (source != null && source instanceof HackrfSource) return source.open(this, this); else { Log.e(LOGTAG, "openSource: sourceType is HACKRF_SOURCE, but source is null or of other type."); return false; } case RTLSDR_SOURCE: if (source != null && source instanceof RtlsdrSource) { // todo: let RTL-SDR manage it dependencies // We might need to start the driver: if (!preferences.getBoolean(getString(R.string.pref_rtlsdr_externalServer), false)) { // start local rtl_tcp instance: try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClassName("marto.rtl_tcp_andro", "com.sdrtouch.rtlsdr.DeviceOpenActivity"); intent.setData(Uri.parse("iqsrc://-a 127.0.0.1 -p 1234 -n 1")); startActivityForResult(intent, RTL2832U_RESULT_CODE); } catch (ActivityNotFoundException e) { Log.e(LOGTAG, "createSource: RTL2832U is not installed"); // Show a dialog that links to the play market: new AlertDialog.Builder(this).setTitle("RTL2832U driver not installed!") .setMessage( "You need to install the (free) RTL2832U driver to use RTL-SDR dongles.") .setPositiveButton("Install from Google Play", (dialog, whichButton) -> { Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=marto.rtl_tcp_andro")); startActivity(marketIntent); }).setNegativeButton("Cancel", (dialog, whichButton) -> { // do nothing }).show(); return false; } } return source.open(this, this); } else { Log.e(LOGTAG, "openSource: sourceType is RTLSDR_SOURCE, but source is null or of other type."); return false; } case HIQSDR_SOURCE: if (source != null && source instanceof HiqsdrSource) return source.open(this, this); else { Log.e(LOGTAG, "openSource: sourceType is HIQSDR_SOURCE, but source is null or of other type."); return false; } default: Log.e(LOGTAG, "openSource: Invalid source type: " + sourceType); return false; } } /** * Will stop the RF Analyzer. This includes shutting down the scheduler (which turns of the * source), the processing loop and the demodulator if running. */ public void stopAnalyzer() { // Stop the Scheduler if running: if (scheduler != null) { // Stop recording in case it is running: stopRecording(); scheduler.stopScheduler(); } // Stop the Processing Loop if running: if (analyzerProcessingLoop != null) analyzerProcessingLoop.stopLoop(); // Stop the Demodulator if running: if (demodulator != null) demodulator.stopDemodulator(); // Wait for the scheduler to stop: if (scheduler != null && !scheduler.getName().equals(Thread.currentThread().getName())) { try { scheduler.join(); } catch (InterruptedException e) { Log.e(LOGTAG, "stopAnalyzer: Error while stopping Scheduler."); } } // Wait for the processing loop to stop if (analyzerProcessingLoop != null) { try { analyzerProcessingLoop.join(); } catch (InterruptedException e) { Log.e(LOGTAG, "stopAnalyzer: Error while stopping Processing Loop."); } } // Wait for the demodulator to stop if (demodulator != null) { try { demodulator.join(); } catch (InterruptedException e) { Log.e(LOGTAG, "stopAnalyzer: Error while stopping Demodulator."); } } running = false; // update action bar icons and titles: updateActionBar(); // allow screen to turn off again: this.runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); } /** * Will start the RF Analyzer. This includes creating a source (if null), open a source * (if not open), starting the scheduler (which starts the source) and starting the * processing loop. */ public void startAnalyzer() { this.stopAnalyzer(); // Stop if running; This assures that we don't end up with multiple instances of the thread loops // Retrieve fft size and frame rate from the preferences int fftSize = Integer.parseInt(preferences.getString(getString(R.string.pref_fftSize), "1024")); int frameRate = Integer.parseInt(preferences.getString(getString(R.string.pref_frameRate), "1")); boolean dynamicFrameRate = preferences.getBoolean(getString(R.string.pref_dynamicFrameRate), true); running = true; if (source == null) { if (!this.createSource()) return; } // check if the source is open. if not, open it! if (!source.isOpen()) { if (!openSource()) { toaster.showLong("Source not available (" + source.getName() + ")"); running = false; return; } return; // we have to wait for the source to become ready... onIQSourceReady() will call startAnalyzer() again... } // Create a new instance of Scheduler and Processing Loop: scheduler = new Scheduler(fftSize, source); analyzerProcessingLoop = new AnalyzerProcessingLoop(analyzerSurface, // Reference to the Analyzer Surface fftSize, // FFT size scheduler.getFftOutputQueue(), // Reference to the input queue for the processing loop scheduler.getFftInputQueue()); // Reference to the buffer-pool-return queue if (dynamicFrameRate) analyzerProcessingLoop.setDynamicFrameRate(true); else { analyzerProcessingLoop.setDynamicFrameRate(false); analyzerProcessingLoop.setFrameRate(frameRate); } // Start both threads: scheduler.start(); analyzerProcessingLoop.start(); scheduler.setChannelFrequency(analyzerSurface.getChannelFrequency()); // Start the demodulator thread: demodulator = new Demodulator(scheduler.getDemodOutputQueue(), scheduler.getDemodInputQueue(), source.getSampledPacketSize()); demodulator.start(); // Set the demodulation mode (will configure the demodulator correctly) this.setDemodulationMode(demodulationMode); // update the action bar icons and titles: updateActionBar(); // Prevent the screen from turning off: this.runOnUiThread(() -> getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); } /** * Will pop up a dialog to let the user choose a demodulation mode. */ private void showDemodulationDialog() { if (scheduler == null || demodulator == null || source == null) { toaster.showLong("Analyzer must be running to change modulation mode"); return; } new AlertDialog.Builder(this).setTitle("Select a demodulation mode:").setSingleChoiceItems( R.array.demodulation_modes, demodulator.getDemodulationMode(), (dialog, which) -> { setDemodulationMode(which); dialog.dismiss(); }).show(); } /** * Will set the modulation mode to the given value. Takes care of adjusting the * scheduler and the demodulator respectively and updates the action bar menu item. * * @param mode Demodulator.DEMODULATION_OFF, *_AM, *_NFM, *_WFM */ public void setDemodulationMode(int mode) { if (scheduler == null || demodulator == null || source == null) { Log.e(LOGTAG, "setDemodulationMode: scheduler/demodulator/source is null"); return; } // (de-)activate demodulation in the scheduler and set the sample rate accordingly: if (mode == Demodulator.DEMODULATION_OFF) { scheduler.setDemodulationActivated(false); } else { if (recordingFile != null && !Demodulator.supportsSampleRate(mode, rxSampleRate.get())) { // We are recording at an incompatible sample rate right now. Log.i(LOGTAG, "setDemodulationMode: Recording is running at " + rxSampleRate.get() + " Sps, but demodulator doesn't support it"); runOnUiThread(() -> toaster .showLong("Recording is running at incompatible sample rate for demodulation!")); return; } // Verify that the source supports the sample rate: if (!Demodulator.supportsSampleRate(mode, rxSampleRate.get())) { Log.e(LOGTAG, "setDemodulationMode: demodulator doesn't support selected sample rate"); toaster.showLong( "Demodulator doesn't support current sample rate: " + rxSampleRate.get() / 1000 + " Ksps)"); scheduler.setDemodulationActivated(false); mode = Demodulator.DEMODULATION_OFF; // deactivate demodulation... } else scheduler.setDemodulationActivated(true); } // set demodulation mode in demodulator: demodulator.setDemodulationMode(mode); this.demodulationMode = mode; // save the setting // disable/enable demodulation view in surface: if (mode == Demodulator.DEMODULATION_OFF) { analyzerSurface.setDemodulationEnabled(false); } else { analyzerSurface.setDemodulationEnabled(true); // will re-adjust channel freq, width and squelch, // if they are outside the current viewport and update the // demodulator via callbacks. analyzerSurface.setShowLowerBand(mode != Demodulator.DEMODULATION_USB); // show lower side band if not USB analyzerSurface.setShowUpperBand(mode != Demodulator.DEMODULATION_LSB); // show upper side band if not LSB } // update action bar: updateActionBar(); } /** * Will pop up a dialog to let the user input a new frequency. * Note: A frequency can be entered either in Hz or in MHz. If the input value * is a number smaller than the maximum frequency of the source in MHz, then it * is interpreted as a frequency in MHz. Otherwise it will be handled as frequency * in Hz. */ private void tuneToFrequency() { if (source == null) return; // calculate max frequency of the source in MHz: final double maxFreqMHz = rxFrequency.getMax() / 1000000f; final LinearLayout ll_view = (LinearLayout) this.getLayoutInflater().inflate(R.layout.tune_to_frequency, null); final EditText et_frequency = (EditText) ll_view.findViewById(R.id.et_tune_to_frequency); final Spinner sp_unit = (Spinner) ll_view.findViewById(R.id.sp_tune_to_frequency_unit); final CheckBox cb_bandwidth = (CheckBox) ll_view.findViewById(R.id.cb_tune_to_frequency_bandwidth); final EditText et_bandwidth = (EditText) ll_view.findViewById(R.id.et_tune_to_frequency_bandwidth); final Spinner sp_bandwidthUnit = (Spinner) ll_view.findViewById(R.id.sp_tune_to_frequency_bandwidth_unit); final TextView tv_warning = (TextView) ll_view.findViewById(R.id.tv_tune_to_frequency_warning); // Show warning if we are currently recording to file: if (recordingFile != null) tv_warning.setVisibility(View.VISIBLE); cb_bandwidth.setOnCheckedChangeListener((buttonView, isChecked) -> { et_bandwidth.setEnabled(isChecked); sp_bandwidthUnit.setEnabled(isChecked); }); cb_bandwidth.toggle(); // to trigger the onCheckedChangeListener at least once to set inital state cb_bandwidth .setChecked(preferences.getBoolean(getString(R.string.pref_tune_to_frequency_setBandwidth), false)); et_bandwidth.setText(preferences.getString(getString(R.string.pref_tune_to_frequency_bandwidth), "1")); sp_unit.setSelection(preferences.getInt(getString(R.string.pref_tune_to_frequency_unit), 0)); sp_bandwidthUnit .setSelection(preferences.getInt(getString(R.string.pref_tune_to_frequency_bandwidthUnit), 0)); new AlertDialog.Builder(this).setTitle("Tune to Frequency") .setMessage( String.format("Frequency is %f MHz. Type a new Frequency: ", rxFrequency.get() / 1000000f)) .setView(ll_view).setPositiveButton("Set", (dialog, whichButton) -> { try { double newFreq = rxFrequency.get() / 1000000f; if (et_frequency.getText().length() != 0) newFreq = Double.valueOf(et_frequency.getText().toString()); switch (sp_unit.getSelectedItemPosition()) { case 0: // MHz newFreq *= 1000000; break; case 1: // KHz newFreq *= 1000; break; default: // Hz } if (newFreq < maxFreqMHz) newFreq = newFreq * 1000000; if (newFreq <= rxFrequency.getMax() && newFreq >= rxFrequency.getMin()) { rxFrequency.set((long) newFreq); analyzerSurface.setVirtualFrequency((long) newFreq); if (demodulationMode != Demodulator.DEMODULATION_OFF) analyzerSurface.setDemodulationEnabled(true); // This will re-adjust the channel freq correctly // Set bandwidth (virtual sample rate): if (cb_bandwidth.isChecked() && et_bandwidth.getText().length() != 0) { float bandwidth = Float.parseFloat(et_bandwidth.getText().toString()); if (sp_bandwidthUnit.getSelectedItemPosition() == 0) //MHz bandwidth *= 1000000; else if (sp_bandwidthUnit.getSelectedItemPosition() == 1) //KHz bandwidth *= 1000; if (bandwidth > rxSampleRate.getMax()) bandwidth = rxFrequency.getMax(); rxSampleRate.set(rxSampleRate.getNextHigherOptimalSampleRate((int) bandwidth)); analyzerSurface.setVirtualSampleRate((int) bandwidth); } // safe preferences: SharedPreferences.Editor edit = preferences.edit(); edit.putInt(getString(R.string.pref_tune_to_frequency_unit), sp_unit.getSelectedItemPosition()); edit.putBoolean(getString(R.string.pref_tune_to_frequency_setBandwidth), cb_bandwidth.isChecked()); edit.putString(getString(R.string.pref_tune_to_frequency_bandwidth), et_bandwidth.getText().toString()); edit.putInt(getString(R.string.pref_tune_to_frequency_bandwidthUnit), sp_bandwidthUnit.getSelectedItemPosition()); edit.apply(); } else { toaster.showLong("Frequency is out of the valid range: " + (long) newFreq + " Hz"); } } catch (NumberFormatException e) { // todo: notify user Log.e(LOGTAG, "tuneToFrequency: Error while setting frequency: " + e.getMessage()); } }).setNegativeButton("Cancel", (dialog, whichButton) -> { // do nothing }).show(); } /** * Will pop up a dialog to let the user adjust gain settings */ private void adjustGain() { if (source == null) return; source.showGainDialog(this, preferences); } public void showRecordingDialog() { if (!running || scheduler == null || demodulator == null || source == null) { toaster.showLong("Analyzer must be running to start recording"); return; } // Check for the WRITE_EXTERNAL_STORAGE permission: if (ContextCompat.checkSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE") != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] { "android.permission.WRITE_EXTERNAL_STORAGE" }, PERMISSION_REQUEST_RECORDING_WRITE_FILES); return; // wait for the permission response (handled in onRequestPermissionResult()) } final String externalDir = Environment.getExternalStorageDirectory().getAbsolutePath(); final int[] supportedSampleRates = rxSampleRate.getSupportedSampleRates(); final double maxFreqMHz = rxFrequency.getMax() / 1000000f; // max frequency of the source in MHz final int sourceType = Integer.parseInt(preferences.getString(getString(R.string.pref_sourceType), "1")); final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); // Get references to the GUI components: final ScrollView view = (ScrollView) this.getLayoutInflater().inflate(R.layout.start_recording, null); final EditText et_filename = (EditText) view.findViewById(R.id.et_recording_filename); final EditText et_frequency = (EditText) view.findViewById(R.id.et_recording_frequency); final Spinner sp_sampleRate = (Spinner) view.findViewById(R.id.sp_recording_sampleRate); final TextView tv_fixedSampleRateHint = (TextView) view.findViewById(R.id.tv_recording_fixedSampleRateHint); final CheckBox cb_stopAfter = (CheckBox) view.findViewById(R.id.cb_recording_stopAfter); final EditText et_stopAfter = (EditText) view.findViewById(R.id.et_recording_stopAfter); final Spinner sp_stopAfter = (Spinner) view.findViewById(R.id.sp_recording_stopAfter); // Setup the sample rate spinner: final ArrayAdapter<Integer> sampleRateAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1); for (int sampR : supportedSampleRates) sampleRateAdapter.add(sampR); sampleRateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); sp_sampleRate.setAdapter(sampleRateAdapter); // Add listener to the frequency textfield, the sample rate spinner and the checkbox: et_frequency.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (et_frequency.getText().length() == 0) return; double freq = Double.parseDouble(et_frequency.getText().toString()); if (freq < maxFreqMHz) freq = freq * 1000000; et_filename.setText(simpleDateFormat.format(new Date()) + "_" + SOURCE_NAMES[sourceType] + "_" + (long) freq + "Hz_" + sp_sampleRate.getSelectedItem() + "Sps.iq"); } }); sp_sampleRate.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (et_frequency.getText().length() == 0) return; double freq = Double.parseDouble(et_frequency.getText().toString()); if (freq < maxFreqMHz) freq = freq * 1000000; et_filename.setText(simpleDateFormat.format(new Date()) + "_" + SOURCE_NAMES[sourceType] + "_" + (long) freq + "Hz_" + sp_sampleRate.getSelectedItem() + "Sps.iq"); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); cb_stopAfter.setOnCheckedChangeListener((buttonView, isChecked) -> { et_stopAfter.setEnabled(isChecked); sp_stopAfter.setEnabled(isChecked); }); // Set default frequency, sample rate and stop after values: et_frequency.setText(Long.toString(analyzerSurface.getVirtualFrequency())); int sampleRateIndex = 0; int lastSampleRate = preferences.getInt(getString(R.string.pref_recordingSampleRate), 1000000); for (; sampleRateIndex < supportedSampleRates.length; sampleRateIndex++) { if (supportedSampleRates[sampleRateIndex] >= lastSampleRate) break; } if (sampleRateIndex >= supportedSampleRates.length) sampleRateIndex = supportedSampleRates.length - 1; sp_sampleRate.setSelection(sampleRateIndex); cb_stopAfter.toggle(); // just to trigger the listener at least once! cb_stopAfter.setChecked(preferences.getBoolean(getString(R.string.pref_recordingStopAfterEnabled), false)); et_stopAfter.setText( Integer.toString(preferences.getInt(getString(R.string.pref_recordingStopAfterValue), 10))); sp_stopAfter.setSelection(preferences.getInt(getString(R.string.pref_recordingStopAfterUnit), 0)); // disable sample rate selection if demodulation is running: if (demodulationMode != Demodulator.DEMODULATION_OFF) { sampleRateAdapter.add(rxSampleRate.get()); // add the current sample rate in case it's not already in the list sp_sampleRate.setSelection(sampleRateAdapter.getPosition(rxSampleRate.get())); // select it sp_sampleRate.setEnabled(false); // disable the spinner tv_fixedSampleRateHint.setVisibility(View.VISIBLE); } // Show dialog: new AlertDialog.Builder(this).setTitle("Start recording").setView(view) .setPositiveButton("Record", (dialog, whichButton) -> { String filename = et_filename.getText().toString(); final int stopAfterUnit = sp_stopAfter.getSelectedItemPosition(); final int stopAfterValue = Integer.parseInt(et_stopAfter.getText().toString()); //todo check filename // Set the frequency in the source: if (et_frequency.getText().length() == 0) return; double freq = Double.parseDouble(et_frequency.getText().toString()); if (freq < maxFreqMHz) freq = freq * 1000000; if (freq <= rxFrequency.getMax() && freq >= rxFrequency.getMin()) rxFrequency.set((long) freq); else { toaster.showLong("Frequency is invalid!"); return; } // Set the sample rate (only if demodulator is off): if (demodulationMode == Demodulator.DEMODULATION_OFF) rxSampleRate.set((Integer) sp_sampleRate.getSelectedItem()); // Open file and start recording: recordingFile = new File(externalDir + "/" + RECORDING_DIR + "/" + filename); recordingFile.getParentFile().mkdir(); // Create directory if it does not yet exist try { scheduler.startRecording(new BufferedOutputStream(new FileOutputStream(recordingFile))); } catch (FileNotFoundException e) { Log.e(LOGTAG, "showRecordingDialog: File not found: " + recordingFile.getAbsolutePath()); } // safe preferences: SharedPreferences.Editor edit = preferences.edit(); edit.putInt(getString(R.string.pref_recordingSampleRate), (Integer) sp_sampleRate.getSelectedItem()); edit.putBoolean(getString(R.string.pref_recordingStopAfterEnabled), cb_stopAfter.isChecked()); edit.putInt(getString(R.string.pref_recordingStopAfterValue), stopAfterValue); edit.putInt(getString(R.string.pref_recordingStopAfterUnit), stopAfterUnit); edit.apply(); analyzerSurface.setRecordingEnabled(true); updateActionBar(); // if stopAfter was selected, start thread to supervise the recording: if (cb_stopAfter.isChecked()) { final String recorderSuperviserName = "Supervisor Thread"; Thread supervisorThread = new Thread(() -> { Log.i(LOGTAG, "recording_superviser: Supervisor Thread started. (Thread: " + recorderSuperviserName + ")"); try { long startTime = System.currentTimeMillis(); boolean stop = false; // We check once per half a second if the stop criteria is met: Thread.sleep(500); while (recordingFile != null && !stop) { switch (stopAfterUnit) { // see arrays.xml - recording_stopAfterUnit case 0: /* MB */ if (recordingFile.length() / 1000000 >= stopAfterValue) stop = true; break; case 1: /* GB */ if (recordingFile.length() / 1000000000 >= stopAfterValue) stop = true; break; case 2: /* sec */ if (System.currentTimeMillis() - startTime >= stopAfterValue * 1000) stop = true; break; case 3: /* min */ if (System.currentTimeMillis() - startTime >= stopAfterValue * 1000 * 60) stop = true; break; } } // stop recording: stopRecording(); } catch (InterruptedException e) { // todo: shouldn't we call stopRecording() here? how about finally{}? Log.e(LOGTAG, "recording_superviser: Interrupted!"); } catch (NullPointerException e) { Log.e(LOGTAG, "recording_superviser: Recording file is null!"); } Log.i(LOGTAG, "recording_superviser: Supervisor Thread stopped. (Thread: " + recorderSuperviserName + ")"); }, recorderSuperviserName); supervisorThread.start(); } }).setNegativeButton("Cancel", (dialog, whichButton) -> { // do nothing }).show().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); } public void stopRecording() { if (scheduler.isRecording()) { scheduler.stopRecording(); } if (recordingFile != null) { final String filename = recordingFile.getAbsolutePath(); final long filesize = recordingFile.length() / 1000000; // file size in MB runOnUiThread(() -> toaster.showLong("Recording stopped: " + filename + " (" + filesize + " MB)")); recordingFile = null; updateActionBar(); } if (analyzerSurface != null) analyzerSurface.setRecordingEnabled(false); } public void showBookmarksDialog() { // show warning toast if recording is running: if (recordingFile != null) toaster.showLong("WARNING: Recording is running!"); new BookmarksDialog(this, this); } public void showInfoDialog() { AlertDialog dialog = new AlertDialog.Builder(this) .setTitle(Html.fromHtml(getString(R.string.info_title, versionName))) .setMessage(Html.fromHtml(getString(R.string.info_msg_body))) .setPositiveButton("OK", (dialog1, whichButton) -> { // Do nothing }).create(); dialog.show(); // make links clickable: ((TextView) dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } @Override public boolean updateDemodulationMode(int newDemodulationMode) { if (scheduler == null || demodulator == null || source == null) { Log.e(LOGTAG, "updateDemodulationMode: scheduler/demodulator/source is null (no demodulation running)"); return false; } setDemodulationMode(newDemodulationMode); return true; } /** * Called by the analyzer surface after the user changed the channel width * * @param newChannelWidth new channel width (single sided) in Hz * @return true if channel width is valid; false if out of range */ @Override public boolean updateChannelWidth(int newChannelWidth) { if (demodulator != null) { if (demodulator.setChannelWidth(newChannelWidth)) { analyzerSurface.setChannelWidth(newChannelWidth); return true; } } return false; } @Override public boolean updateChannelFrequency(long newChannelFrequency) { if (scheduler != null) { scheduler.setChannelFrequency(newChannelFrequency); analyzerSurface.setChannelFrequency(newChannelFrequency); return true; } return false; } public boolean updateSourceFrequency(long newSourceFrequency) { if (source != null && newSourceFrequency <= rxFrequency.getMax() && newSourceFrequency >= rxFrequency.getMin()) { rxFrequency.set(newSourceFrequency); analyzerSurface.setVirtualFrequency(newSourceFrequency); return true; } return false; } public boolean updateSampleRate(int newSampleRate) { if (source != null) { if (scheduler == null || !scheduler.isRecording()) { rxSampleRate.set(newSampleRate); return true; } } return false; } @Override public void updateSquelch(float newSquelch) { analyzerSurface.setSquelch(newSquelch); } @Override public boolean updateSquelchSatisfied(boolean squelchSatisfied) { if (scheduler != null) { scheduler.setSquelchSatisfied(squelchSatisfied); return true; } return false; } @Override public int requestCurrentChannelWidth() { if (demodulator != null) return demodulator.getChannelWidth(); else return -1; } public long requestCurrentChannelFrequency() { if (scheduler != null) return scheduler.getChannelFrequency(); else return -1; } public int requestCurrentDemodulationMode() { return demodulationMode; } public float requestCurrentSquelch() { if (analyzerSurface != null) return analyzerSurface.getSquelch(); else return Float.NaN; } public long requestCurrentSourceFrequency() { if (source != null) return rxFrequency.get(); else return -1; } public int requestCurrentSampleRate() { if (source != null) return rxSampleRate.get(); else return -1; } public long requestMaxSourceFrequency() { if (source != null) return rxFrequency.getMax(); else return -1; } public int[] requestSupportedSampleRates() { if (source != null) return rxSampleRate.getSupportedSampleRates(); else return null; } }