com.mantz_it.rfanalyzer.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.mantz_it.rfanalyzer.MainActivity.java

Source

package com.mantz_it.rfanalyzer;

import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
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.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.CompoundButton;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * <h1>RF Analyzer - Main Activity</h1>
 *
 * Module:      MainActivity.java
 * Description: Main Activity of the RF Analyzer
 *
 * @author Dennis Mantz
 *
 * Copyright (C) 2014 Dennis Mantz
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
 *
 * 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.
 *
 * 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.
 *
 * 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 IQSourceInterface.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 IQSourceInterface source = 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 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
    public static final int PERMISSION_REQUEST_FILE_SOURCE_READ_FILES = 1111; // arbitrary value, used when requesting
    // permission to open file for the file source
    public static final int PERMISSION_REQUEST_RECORDING_WRITE_FILES = 1112; // arbitrary value, used when requesting
    // permission to write file for the recording feature
    private static final int FILE_SOURCE = 0;
    private static final int HACKRF_SOURCE = 1;
    private static final int RTLSDR_SOURCE = 2;
    private static final String[] SOURCE_NAMES = new String[] { "filesource", "hackrf", "rtlsdr" };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        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);

        // 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.");
        } 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.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.valueOf(preferences.getString(getString(R.string.pref_colorMapType), "4")));
        analyzerSurface.setFftDrawingType(
                Integer.valueOf(preferences.getString(getString(R.string.pref_fftDrawingType), "2")));
        analyzerSurface.setFftRatio(
                Float.valueOf(preferences.getString(getString(R.string.pref_spectrumWaterfallRatio), "0.5")));
        analyzerSurface.setFontSize(Integer.valueOf(preferences.getString(getString(R.string.pref_fontSize), "2")));
        analyzerSurface.setShowDebugInformation(
                preferences.getBoolean(getString(R.string.pref_showDebugInformation), false));

        // 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.valueOf(
                            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? ...)
                Toast.makeText(MainActivity.this, "Stopping and restarting RTL2832U driver...", Toast.LENGTH_SHORT)
                        .show();

                // 2) Delayed start of the Analyzer:
                Thread timer = new Thread() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1500);
                            startAnalyzer();
                        } catch (InterruptedException e) {
                            Log.e(LOGTAG, "onCreate: (timer thread): Interrupted while sleeping.");
                        }
                    }
                };
                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.valueOf(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");
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putBoolean(getString(R.string.save_state_running), running);
        outState.putInt(getString(R.string.save_state_demodulatorMode), demodulationMode);
        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(new Runnable() {
            @Override
            public void run() {
                // 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) {
            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)));
            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...
        }
    }

    @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();
            edit.putLong(getString(R.string.pref_frequency), source.getFrequency());
            edit.putInt(getString(R.string.pref_sampleRate), source.getSampleRate());
            edit.putInt(getString(R.string.pref_virtualSampleRate), analyzerSurface.getVirtualSampleRate());
            edit.putLong(getString(R.string.pref_virtualFrequency), analyzerSurface.getVirtualFrequency());
            edit.commit();
        }
    }

    @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) {
                    Toast.makeText(MainActivity.this,
                            "Error with Source [" + source.getName() + "]: " + errorMsg + " (" + errorId + ")"
                                    + (detailedDescription != null
                                            ? ": " + detailedDescription + " (" + exceptionCode + ")"
                                            : ""),
                            Toast.LENGTH_LONG).show();
                    source.close();
                }
            }
            break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], 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;
        }
        }
    }

    @Override
    public void onIQSourceReady(IQSourceInterface source) { // is called after source.open()
        if (running)
            startAnalyzer(); // will start the processing loop, scheduler and source
    }

    @Override
    public void onIQSourceError(final IQSourceInterface source, final String message) {
        this.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this, "Error with Source [" + source.getName() + "]: " + message,
                        Toast.LENGTH_LONG).show();
            }
        });
        stopAnalyzer();

        if (this.source != null && this.source.isOpen())
            this.source.close();
    }

    /**
     * Will check if any preference conflicts with the current state of the app and fix it
     */
    public void checkForChangedPreferences() {
        // Source Type (this is pretty complex as we have to check each type individually):
        int sourceType = Integer.valueOf(preferences.getString(getString(R.string.pref_sourceType), "1"));
        if (source != null) {
            switch (sourceType) {
            case FILE_SOURCE:
                if (!(source instanceof FileIQSource)) {
                    source.close();
                    createSource();
                } else {
                    long freq = Integer.valueOf(
                            preferences.getString(getString(R.string.pref_filesource_frequency), "97000000"));
                    int sampRate = Integer.valueOf(
                            preferences.getString(getString(R.string.pref_filesource_sampleRate), "2000000"));
                    String fileName = preferences.getString(getString(R.string.pref_filesource_file), "");
                    int fileFormat = Integer
                            .valueOf(preferences.getString(getString(R.string.pref_filesource_format), "0"));
                    boolean repeat = preferences.getBoolean(getString(R.string.pref_filesource_repeat), false);
                    if (freq != source.getFrequency() || sampRate != source.getSampleRate()
                            || !fileName.equals(((FileIQSource) source).getFilename())
                            || repeat != ((FileIQSource) source).isRepeat()
                            || fileFormat != ((FileIQSource) source).getFileFormat()) {
                        source.close();
                        createSource();
                    }
                }
                break;
            case HACKRF_SOURCE:
                if (!(source instanceof HackrfSource)) {
                    source.close();
                    createSource();
                } else {
                    // overwrite hackrf source settings if changed:
                    boolean amp = preferences.getBoolean(getString(R.string.pref_hackrf_amplifier), false);
                    boolean antennaPower = preferences.getBoolean(getString(R.string.pref_hackrf_antennaPower),
                            false);
                    int frequencyOffset = Integer
                            .valueOf(preferences.getString(getString(R.string.pref_hackrf_frequencyOffset), "0"));
                    if (((HackrfSource) source).isAmplifierOn() != amp)
                        ((HackrfSource) source).setAmplifier(amp);
                    if (((HackrfSource) source).isAntennaPowerOn() != antennaPower)
                        ((HackrfSource) source).setAntennaPower(antennaPower);
                    if (((HackrfSource) source).getFrequencyOffset() != frequencyOffset)
                        ((HackrfSource) source).setFrequencyOffset(frequencyOffset);
                }
                break;
            case RTLSDR_SOURCE:
                if (!(source instanceof RtlsdrSource)) {
                    source.close();
                    createSource();
                } else {
                    // Check if ip or port has changed and recreate source if necessary:
                    String ip = preferences.getString(getString(R.string.pref_rtlsdr_ip), "");
                    int port = Integer.valueOf(preferences.getString(getString(R.string.pref_rtlsdr_port), "1234"));
                    boolean externalServer = preferences.getBoolean(getString(R.string.pref_rtlsdr_externalServer),
                            false);
                    if (externalServer) {
                        if (!ip.equals(((RtlsdrSource) source).getIpAddress())
                                || port != ((RtlsdrSource) source).getPort()) {
                            source.close();
                            createSource();
                            return;
                        }
                    } else {
                        if (!((RtlsdrSource) source).getIpAddress().equals("127.0.0.1")
                                || 1234 != ((RtlsdrSource) source).getPort()) {
                            source.close();
                            createSource();
                            return;
                        }
                    }

                    // otherwise just overwrite rtl-sdr source settings if changed:
                    int frequencyCorrection = Integer.valueOf(
                            preferences.getString(getString(R.string.pref_rtlsdr_frequencyCorrection), "0"));
                    int frequencyOffset = Integer
                            .valueOf(preferences.getString(getString(R.string.pref_rtlsdr_frequencyOffset), "0"));
                    if (frequencyCorrection != ((RtlsdrSource) source).getFrequencyCorrection())
                        ((RtlsdrSource) source).setFrequencyCorrection(frequencyCorrection);
                    if (((RtlsdrSource) source).getFrequencyOffset() != frequencyOffset)
                        ((RtlsdrSource) source).setFrequencyOffset(frequencyOffset);
                    ((RtlsdrSource) source).setDirectSampling(Integer
                            .valueOf(preferences.getString(getString(R.string.pref_rtlsdr_directSamp), "0")));
                }
                break;
            default:
            }
        }

        if (analyzerSurface != null) {
            // 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.valueOf(preferences.getString(getString(R.string.pref_colorMapType), "4")));
            analyzerSurface.setFftDrawingType(
                    Integer.valueOf(preferences.getString(getString(R.string.pref_fftDrawingType), "2")));
            analyzerSurface.setAverageLength(
                    Integer.valueOf(preferences.getString(getString(R.string.pref_averaging), "0")));
            analyzerSurface.setPeakHoldEnabled(preferences.getBoolean(getString(R.string.pref_peakHold), false));
            analyzerSurface.setFftRatio(
                    Float.valueOf(preferences.getString(getString(R.string.pref_spectrumWaterfallRatio), "0.5")));
            analyzerSurface
                    .setFontSize(Integer.valueOf(preferences.getString(getString(R.string.pref_fontSize), "2")));
            analyzerSurface.setShowDebugInformation(
                    preferences.getBoolean(getString(R.string.pref_showDebugInformation), false));
            analyzerSurface.setDisplayFrequencyUnit(
                    Integer.valueOf(preferences.getString(getString(R.string.pref_surface_unit), "1000000")));
        }

        // Screen Orientation:
        String screenOrientation = preferences.getString(getString(R.string.pref_screenOrientation), "auto");
        if (screenOrientation.equals("auto"))
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
        else if (screenOrientation.equals("landscape"))
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        else if (screenOrientation.equals("portrait"))
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        else if (screenOrientation.equals("reverse_landscape"))
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
        else if (screenOrientation.equals("reverse_portrait"))
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
    }

    /**
     * Will create a IQ Source instance according to the user settings.
     *
     * @return true on success; false on error
     */
    public boolean createSource() {
        long frequency;
        int sampleRate;
        int sourceType = Integer.valueOf(preferences.getString(getString(R.string.pref_sourceType), "1"));

        switch (sourceType) {
        case FILE_SOURCE:
            // Create IQ Source (filesource)
            try {
                frequency = Integer
                        .valueOf(preferences.getString(getString(R.string.pref_filesource_frequency), "97000000"));
                sampleRate = Integer
                        .valueOf(preferences.getString(getString(R.string.pref_filesource_sampleRate), "2000000"));
            } catch (NumberFormatException e) {
                this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this, "File Source: Wrong format of frequency or sample rate",
                                Toast.LENGTH_LONG).show();
                    }
                });
                return false;
            }
            String filename = preferences.getString(getString(R.string.pref_filesource_file), "");
            int fileFormat = Integer
                    .valueOf(preferences.getString(getString(R.string.pref_filesource_format), "0"));
            boolean repeat = preferences.getBoolean(getString(R.string.pref_filesource_repeat), false);
            source = new FileIQSource(filename, sampleRate, frequency, 16384, repeat, fileFormat);
            break;
        case HACKRF_SOURCE:
            // Create HackrfSource
            source = new HackrfSource();
            source.setFrequency(preferences.getLong(getString(R.string.pref_frequency), 97000000));
            source.setSampleRate(
                    preferences.getInt(getString(R.string.pref_sampleRate), HackrfSource.MAX_SAMPLERATE));
            ((HackrfSource) source).setVgaRxGain(preferences.getInt(getString(R.string.pref_hackrf_vgaRxGain),
                    HackrfSource.MAX_VGA_RX_GAIN / 2));
            ((HackrfSource) source).setLnaGain(
                    preferences.getInt(getString(R.string.pref_hackrf_lnaGain), HackrfSource.MAX_LNA_GAIN / 2));
            ((HackrfSource) source)
                    .setAmplifier(preferences.getBoolean(getString(R.string.pref_hackrf_amplifier), false));
            ((HackrfSource) source)
                    .setAntennaPower(preferences.getBoolean(getString(R.string.pref_hackrf_antennaPower), false));
            ((HackrfSource) source).setFrequencyOffset(
                    Integer.valueOf(preferences.getString(getString(R.string.pref_hackrf_frequencyOffset), "0")));
            break;
        case RTLSDR_SOURCE:
            // Create RtlsdrSource
            if (preferences.getBoolean(getString(R.string.pref_rtlsdr_externalServer), false))
                source = new RtlsdrSource(preferences.getString(getString(R.string.pref_rtlsdr_ip), ""),
                        Integer.valueOf(preferences.getString(getString(R.string.pref_rtlsdr_port), "1234")));
            else {
                source = new RtlsdrSource("127.0.0.1", 1234);
            }

            frequency = preferences.getLong(getString(R.string.pref_frequency), 97000000);
            sampleRate = preferences.getInt(getString(R.string.pref_sampleRate), source.getMaxSampleRate());
            if (sampleRate > 2000000) // might be the case after switching over from HackRF
                sampleRate = 2000000;
            source.setFrequency(frequency);
            source.setSampleRate(sampleRate);

            ((RtlsdrSource) source).setFrequencyCorrection(Integer
                    .valueOf(preferences.getString(getString(R.string.pref_rtlsdr_frequencyCorrection), "0")));
            ((RtlsdrSource) source).setFrequencyOffset(
                    Integer.valueOf(preferences.getString(getString(R.string.pref_rtlsdr_frequencyOffset), "0")));
            ((RtlsdrSource) source)
                    .setManualGain(preferences.getBoolean(getString(R.string.pref_rtlsdr_manual_gain), false));
            ((RtlsdrSource) source)
                    .setAutomaticGainControl(preferences.getBoolean(getString(R.string.pref_rtlsdr_agc), false));
            if (((RtlsdrSource) source).isManualGain()) {
                ((RtlsdrSource) source).setGain(preferences.getInt(getString(R.string.pref_rtlsdr_gain), 0));
                ((RtlsdrSource) source).setIFGain(preferences.getInt(getString(R.string.pref_rtlsdr_ifGain), 0));
            }
            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.valueOf(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) {
                // 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",
                                        new DialogInterface.OnClickListener() {
                                            public void onClick(DialogInterface dialog, int whichButton) {
                                                Intent marketIntent = new Intent(Intent.ACTION_VIEW,
                                                        Uri.parse("market://details?id=marto.rtl_tcp_andro"));
                                                startActivity(marketIntent);
                                            }
                                        })
                                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                                    public void onClick(DialogInterface dialog, int 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;
            }
        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, "startAnalyzer: Error while stopping Scheduler.");
            }
        }

        // Wait for the processing loop to stop
        if (analyzerProcessingLoop != null) {
            try {
                analyzerProcessingLoop.join();
            } catch (InterruptedException e) {
                Log.e(LOGTAG, "startAnalyzer: Error while stopping Processing Loop.");
            }
        }

        // Wait for the demodulator to stop
        if (demodulator != null) {
            try {
                demodulator.join();
            } catch (InterruptedException e) {
                Log.e(LOGTAG, "startAnalyzer: Error while stopping Demodulator.");
            }
        }

        running = false;

        // update action bar icons and titles:
        updateActionBar();

        // allow screen to turn off again:
        this.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                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.valueOf(preferences.getString(getString(R.string.pref_fftSize), "1024"));
        int frameRate = Integer.valueOf(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()) {
                Toast.makeText(MainActivity.this, "Source not available (" + source.getName() + ")",
                        Toast.LENGTH_LONG).show();
                running = false;
                return;
            }
            return; // we have to wait for the source to become ready... onIQSourceReady() will call startAnalyzer() again...
        }

        // Set direct sampling mode according to preference
        if (Integer.valueOf(preferences.getString(getString(R.string.pref_sourceType), "1")) == RTLSDR_SOURCE) {
            ((RtlsdrSource) source).setDirectSampling(
                    Integer.valueOf(preferences.getString(getString(R.string.pref_rtlsdr_directSamp), "0")));
        }

        source.setSampleRate(source.getNextHigherOptimalSampleRate(
                preferences.getInt(getString(R.string.pref_virtualSampleRate), 1000000)));
        analyzerSurface
                .setVirtualSampleRate(preferences.getInt(getString(R.string.pref_virtualSampleRate), 1000000));
        analyzerSurface
                .setVirtualFrequency(preferences.getLong(getString(R.string.pref_virtualFrequency), 97000000));

        // 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.getPacketSize());
        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(new Runnable() {
            @Override
            public void run() {
                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) {
            Toast.makeText(MainActivity.this, "Analyzer must be running to change modulation mode",
                    Toast.LENGTH_LONG).show();
            return;
        }

        new AlertDialog.Builder(this).setTitle("Select a demodulation mode:")
                .setSingleChoiceItems(R.array.demodulation_modes, demodulator.getDemodulationMode(),
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int 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 && source.getSampleRate() != Demodulator.INPUT_RATE) {
                // We are recording at an incompatible sample rate right now.
                Log.i(LOGTAG, "setDemodulationMode: Recording is running at " + source.getSampleRate()
                        + " Sps. Can't start demodulation.");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,
                                "Recording is running at incompatible sample rate for demodulation!",
                                Toast.LENGTH_LONG).show();
                    }
                });
                return;
            }

            // adjust sample rate of the source:
            source.setSampleRate(Demodulator.INPUT_RATE);

            // Verify that the source supports the sample rate:
            if (source.getSampleRate() != Demodulator.INPUT_RATE) {
                Log.e(LOGTAG, "setDemodulationMode: cannot adjust source sample rate!");
                Toast.makeText(MainActivity.this,
                        "Source does not support the sample rate necessary for demodulation ("
                                + Demodulator.INPUT_RATE / 1000000 + " Msps)",
                        Toast.LENGTH_LONG).show();
                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 = source.getMaxFrequency() / 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(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean 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: ",
                        source.getFrequency() / 1000000f))
                .setView(ll_view).setPositiveButton("Set", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        try {
                            double newFreq = source.getFrequency();
                            if (et_frequency.getText().length() != 0)
                                newFreq = Double.valueOf(et_frequency.getText().toString());
                            if (sp_unit.getSelectedItemPosition() == 0) //MHz
                                newFreq *= 1000000;
                            else if (sp_unit.getSelectedItemPosition() == 1) //KHz
                                newFreq *= 1000;
                            if (newFreq <= source.getMaxFrequency() && newFreq >= source.getMinFrequency()) {
                                source.setFrequency((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.valueOf(et_bandwidth.getText().toString());
                                    if (sp_bandwidthUnit.getSelectedItemPosition() == 0) //MHz
                                        bandwidth *= 1000000;
                                    else if (sp_bandwidthUnit.getSelectedItemPosition() == 1) //KHz
                                        bandwidth *= 1000;
                                    if (bandwidth > source.getMaxSampleRate())
                                        bandwidth = source.getMaxFrequency();
                                    source.setSampleRate(source.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 {
                                Toast.makeText(MainActivity.this,
                                        "Frequency is out of the valid range: " + (long) newFreq + " Hz",
                                        Toast.LENGTH_LONG).show();
                            }
                        } catch (NumberFormatException e) {
                            Log.e(LOGTAG, "tuneToFrequency: Error while setting frequency: " + e.getMessage());
                        }
                    }
                }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        // do nothing
                    }
                }).show();
    }

    /**
     * Will pop up a dialog to let the user adjust gain settings
     */
    private void adjustGain() {
        if (source == null)
            return;

        int sourceType = Integer.valueOf(preferences.getString(getString(R.string.pref_sourceType), "1"));
        switch (sourceType) {
        case FILE_SOURCE:
            Toast.makeText(this, getString(R.string.filesource_doesnt_support_gain), Toast.LENGTH_LONG).show();
            break;
        case HACKRF_SOURCE:
            // Prepare layout:
            final LinearLayout view_hackrf = (LinearLayout) this.getLayoutInflater().inflate(R.layout.hackrf_gain,
                    null);
            final SeekBar sb_hackrf_vga = (SeekBar) view_hackrf.findViewById(R.id.sb_hackrf_vga_gain);
            final SeekBar sb_hackrf_lna = (SeekBar) view_hackrf.findViewById(R.id.sb_hackrf_lna_gain);
            final TextView tv_hackrf_vga = (TextView) view_hackrf.findViewById(R.id.tv_hackrf_vga_gain);
            final TextView tv_hackrf_lna = (TextView) view_hackrf.findViewById(R.id.tv_hackrf_lna_gain);
            sb_hackrf_vga.setMax(HackrfSource.MAX_VGA_RX_GAIN / HackrfSource.VGA_RX_GAIN_STEP_SIZE);
            sb_hackrf_lna.setMax(HackrfSource.MAX_LNA_GAIN / HackrfSource.LNA_GAIN_STEP_SIZE);
            sb_hackrf_vga.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    tv_hackrf_vga.setText("" + progress * HackrfSource.VGA_RX_GAIN_STEP_SIZE);
                    ((HackrfSource) source).setVgaRxGain(progress * HackrfSource.VGA_RX_GAIN_STEP_SIZE);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
            sb_hackrf_lna.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    tv_hackrf_lna.setText("" + progress * HackrfSource.LNA_GAIN_STEP_SIZE);
                    ((HackrfSource) source).setLnaGain(progress * HackrfSource.LNA_GAIN_STEP_SIZE);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
            sb_hackrf_vga.setProgress(((HackrfSource) source).getVgaRxGain() / HackrfSource.VGA_RX_GAIN_STEP_SIZE);
            sb_hackrf_lna.setProgress(((HackrfSource) source).getLnaGain() / HackrfSource.LNA_GAIN_STEP_SIZE);

            // Show dialog:
            AlertDialog hackrfDialog = new AlertDialog.Builder(this).setTitle("Adjust Gain Settings")
                    .setView(view_hackrf).setPositiveButton("Set", new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            // safe preferences:
                            SharedPreferences.Editor edit = preferences.edit();
                            edit.putInt(getString(R.string.pref_hackrf_vgaRxGain),
                                    sb_hackrf_vga.getProgress() * HackrfSource.VGA_RX_GAIN_STEP_SIZE);
                            edit.putInt(getString(R.string.pref_hackrf_lnaGain),
                                    sb_hackrf_lna.getProgress() * HackrfSource.LNA_GAIN_STEP_SIZE);
                            edit.apply();
                        }
                    }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            // do nothing
                        }
                    }).create();
            hackrfDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                @Override
                public void onDismiss(DialogInterface dialog) {
                    // sync source with (new/old) settings
                    int vgaRxGain = preferences.getInt(getString(R.string.pref_hackrf_vgaRxGain),
                            HackrfSource.MAX_VGA_RX_GAIN / 2);
                    int lnaGain = preferences.getInt(getString(R.string.pref_hackrf_lnaGain),
                            HackrfSource.MAX_LNA_GAIN / 2);
                    if (((HackrfSource) source).getVgaRxGain() != vgaRxGain)
                        ((HackrfSource) source).setVgaRxGain(vgaRxGain);
                    if (((HackrfSource) source).getLnaGain() != lnaGain)
                        ((HackrfSource) source).setLnaGain(lnaGain);
                }
            });
            hackrfDialog.show();
            hackrfDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
            break;
        case RTLSDR_SOURCE:
            final int[] possibleGainValues = ((RtlsdrSource) source).getPossibleGainValues();
            final int[] possibleIFGainValues = ((RtlsdrSource) source).getPossibleIFGainValues();
            if (possibleGainValues.length <= 1 && possibleIFGainValues.length <= 1) {
                Toast.makeText(MainActivity.this, source.getName() + " does not support gain adjustment!",
                        Toast.LENGTH_LONG).show();
            }
            // Prepare layout:
            final LinearLayout view_rtlsdr = (LinearLayout) this.getLayoutInflater().inflate(R.layout.rtlsdr_gain,
                    null);
            final LinearLayout ll_rtlsdr_gain = (LinearLayout) view_rtlsdr.findViewById(R.id.ll_rtlsdr_gain);
            final LinearLayout ll_rtlsdr_ifgain = (LinearLayout) view_rtlsdr.findViewById(R.id.ll_rtlsdr_ifgain);
            final Switch sw_rtlsdr_manual_gain = (Switch) view_rtlsdr.findViewById(R.id.sw_rtlsdr_manual_gain);
            final CheckBox cb_rtlsdr_agc = (CheckBox) view_rtlsdr.findViewById(R.id.cb_rtlsdr_agc);
            final SeekBar sb_rtlsdr_gain = (SeekBar) view_rtlsdr.findViewById(R.id.sb_rtlsdr_gain);
            final SeekBar sb_rtlsdr_ifGain = (SeekBar) view_rtlsdr.findViewById(R.id.sb_rtlsdr_ifgain);
            final TextView tv_rtlsdr_gain = (TextView) view_rtlsdr.findViewById(R.id.tv_rtlsdr_gain);
            final TextView tv_rtlsdr_ifGain = (TextView) view_rtlsdr.findViewById(R.id.tv_rtlsdr_ifgain);

            // Assign current gain:
            int gainIndex = 0;
            int ifGainIndex = 0;
            for (int i = 0; i < possibleGainValues.length; i++) {
                if (((RtlsdrSource) source).getGain() == possibleGainValues[i]) {
                    gainIndex = i;
                    break;
                }
            }
            for (int i = 0; i < possibleIFGainValues.length; i++) {
                if (((RtlsdrSource) source).getIFGain() == possibleIFGainValues[i]) {
                    ifGainIndex = i;
                    break;
                }
            }
            sb_rtlsdr_gain.setMax(possibleGainValues.length - 1);
            sb_rtlsdr_ifGain.setMax(possibleIFGainValues.length - 1);
            sb_rtlsdr_gain.setProgress(gainIndex);
            sb_rtlsdr_ifGain.setProgress(ifGainIndex);
            tv_rtlsdr_gain.setText("" + possibleGainValues[gainIndex]);
            tv_rtlsdr_ifGain.setText("" + possibleIFGainValues[ifGainIndex]);

            // Assign current manual gain and agc setting
            sw_rtlsdr_manual_gain.setChecked(((RtlsdrSource) source).isManualGain());
            cb_rtlsdr_agc.setChecked(((RtlsdrSource) source).isAutomaticGainControl());

            // Add listener to gui elements:
            sw_rtlsdr_manual_gain.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    sb_rtlsdr_gain.setEnabled(isChecked);
                    tv_rtlsdr_gain.setEnabled(isChecked);
                    sb_rtlsdr_ifGain.setEnabled(isChecked);
                    tv_rtlsdr_ifGain.setEnabled(isChecked);
                    ((RtlsdrSource) source).setManualGain(isChecked);
                    if (isChecked) {
                        ((RtlsdrSource) source).setGain(possibleGainValues[sb_rtlsdr_gain.getProgress()]);
                        ((RtlsdrSource) source).setIFGain(possibleIFGainValues[sb_rtlsdr_ifGain.getProgress()]);
                    }
                }
            });
            cb_rtlsdr_agc.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    ((RtlsdrSource) source).setAutomaticGainControl(isChecked);
                }
            });
            sb_rtlsdr_gain.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    tv_rtlsdr_gain.setText("" + possibleGainValues[progress]);
                    ((RtlsdrSource) source).setGain(possibleGainValues[progress]);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
            sb_rtlsdr_ifGain.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    tv_rtlsdr_ifGain.setText("" + possibleIFGainValues[progress]);
                    ((RtlsdrSource) source).setIFGain(possibleIFGainValues[progress]);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });

            // Disable gui elements if gain cannot be adjusted:
            if (possibleGainValues.length <= 1)
                ll_rtlsdr_gain.setVisibility(View.GONE);
            if (possibleIFGainValues.length <= 1)
                ll_rtlsdr_ifgain.setVisibility(View.GONE);

            if (!sw_rtlsdr_manual_gain.isChecked()) {
                sb_rtlsdr_gain.setEnabled(false);
                tv_rtlsdr_gain.setEnabled(false);
                sb_rtlsdr_ifGain.setEnabled(false);
                tv_rtlsdr_ifGain.setEnabled(false);
            }

            // Show dialog:
            AlertDialog rtlsdrDialog = new AlertDialog.Builder(this).setTitle("Adjust Gain Settings")
                    .setView(view_rtlsdr).setPositiveButton("Set", new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            // safe preferences:
                            SharedPreferences.Editor edit = preferences.edit();
                            edit.putBoolean(getString(R.string.pref_rtlsdr_manual_gain),
                                    sw_rtlsdr_manual_gain.isChecked());
                            edit.putBoolean(getString(R.string.pref_rtlsdr_agc), cb_rtlsdr_agc.isChecked());
                            edit.putInt(getString(R.string.pref_rtlsdr_gain),
                                    possibleGainValues[sb_rtlsdr_gain.getProgress()]);
                            edit.putInt(getString(R.string.pref_rtlsdr_ifGain),
                                    possibleIFGainValues[sb_rtlsdr_ifGain.getProgress()]);
                            edit.apply();
                        }
                    }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            // do nothing
                        }
                    }).create();
            rtlsdrDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                @Override
                public void onDismiss(DialogInterface dialog) {
                    boolean manualGain = preferences.getBoolean(getString(R.string.pref_rtlsdr_manual_gain), false);
                    boolean agc = preferences.getBoolean(getString(R.string.pref_rtlsdr_agc), false);
                    int gain = preferences.getInt(getString(R.string.pref_rtlsdr_gain), 0);
                    int ifGain = preferences.getInt(getString(R.string.pref_rtlsdr_ifGain), 0);
                    ((RtlsdrSource) source).setGain(gain);
                    ((RtlsdrSource) source).setIFGain(ifGain);
                    ((RtlsdrSource) source).setManualGain(manualGain);
                    ((RtlsdrSource) source).setAutomaticGainControl(agc);
                    if (manualGain) {
                        // Note: This is a workaround. After setting manual gain to true we must
                        // rewrite the manual gain values:
                        ((RtlsdrSource) source).setGain(gain);
                        ((RtlsdrSource) source).setIFGain(ifGain);
                    }
                }
            });
            rtlsdrDialog.show();
            rtlsdrDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
            break;
        default:
            Log.e(LOGTAG, "adjustGain: Invalid source type: " + sourceType);
            break;
        }
    }

    public void showRecordingDialog() {
        if (!running || scheduler == null || demodulator == null || source == null) {
            Toast.makeText(MainActivity.this, "Analyzer must be running to start recording", Toast.LENGTH_LONG)
                    .show();
            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 = source.getSupportedSampleRates();
        final double maxFreqMHz = source.getMaxFrequency() / 1000000f; // max frequency of the source in MHz
        final int sourceType = Integer.valueOf(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<Integer>(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.valueOf(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.valueOf(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(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                et_stopAfter.setEnabled(isChecked);
                sp_stopAfter.setEnabled(isChecked);
            }
        });

        // Set default frequency, sample rate and stop after values:
        et_frequency.setText("" + 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("" + 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(source.getSampleRate()); // add the current sample rate in case it's not already in the list
            sp_sampleRate.setSelection(sampleRateAdapter.getPosition(source.getSampleRate())); // 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", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        String filename = et_filename.getText().toString();
                        final int stopAfterUnit = sp_stopAfter.getSelectedItemPosition();
                        final int stopAfterValue = Integer.valueOf(et_stopAfter.getText().toString());
                        //todo check filename

                        // Set the frequency in the source:
                        if (et_frequency.getText().length() == 0)
                            return;
                        double freq = Double.valueOf(et_frequency.getText().toString());
                        if (freq < maxFreqMHz)
                            freq = freq * 1000000;
                        if (freq <= source.getMaxFrequency() && freq >= source.getMinFrequency())
                            source.setFrequency((long) freq);
                        else {
                            Toast.makeText(MainActivity.this, "Frequency is invalid!", Toast.LENGTH_LONG).show();
                            return;
                        }

                        // Set the sample rate (only if demodulator is off):
                        if (demodulationMode == Demodulator.DEMODULATION_OFF)
                            source.setSampleRate((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()) {
                            Thread supervisorThread = new Thread() {
                                @Override
                                public void run() {
                                    Log.i(LOGTAG, "recording_superviser: Supervisor Thread started. (Thread: "
                                            + this.getName() + ")");
                                    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) {
                                        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: "
                                            + this.getName() + ")");
                                }
                            };
                            supervisorThread.start();
                        }
                    }
                }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int 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(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(MainActivity.this, "Recording stopped: " + filename + " (" + filesize + " MB)",
                            Toast.LENGTH_LONG).show();
                }
            });
            recordingFile = null;
            updateActionBar();
        }
        if (analyzerSurface != null)
            analyzerSurface.setRecordingEnabled(false);
    }

    public void showBookmarksDialog() {
        // show warning toast if recording is running:
        if (recordingFile != null)
            Toast.makeText(this, "WARNING: Recording is running!", Toast.LENGTH_LONG).show();
        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", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        // Do nothing
                    }
                }).create();
        dialog.show();

        // make links clickable:
        ((TextView) dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
    }

    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 <= source.getMaxFrequency()
                && newSourceFrequency >= source.getMinFrequency()) {
            source.setFrequency(newSourceFrequency);
            analyzerSurface.setVirtualFrequency(newSourceFrequency);
            return true;
        }
        return false;
    }

    public boolean updateSampleRate(int newSampleRate) {
        if (source != null) {
            if (scheduler == null || !scheduler.isRecording()) {
                source.setSampleRate(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 source.getFrequency();
        else
            return -1;
    }

    public int requestCurrentSampleRate() {
        if (source != null)
            return source.getSampleRate();
        else
            return -1;
    }

    public long requestMaxSourceFrequency() {
        if (source != null)
            return source.getMaxFrequency();
        else
            return -1;
    }

    public int[] requestSupportedSampleRates() {
        if (source != null)
            return source.getSupportedSampleRates();
        else
            return null;
    }
}