diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..3c7a6191 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,12 @@ +.cxx +.externalNativeBuild +build/ +*.iml + +.idea +.gradle +local.properties + +# Android Studio puts a Gradle wrapper here, that we don't want: +gradle/ +gradlew* diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..ed4054db --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,64 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 28 + buildToolsVersion "30.0.3" + ndkVersion "23.0.7599858" + defaultConfig { + applicationId "org.sdrpp.sdrpp" + minSdkVersion 28 + targetSdkVersion 28 + versionCode 1 + versionName "1.1.0" + + externalNativeBuild { + cmake { + arguments "-DOPT_BACKEND_GLFW=OFF", "-DOPT_BACKEND_ANDROID=ON", "-DOPT_BUILD_SOAPY_SOURCE=OFF", "-DOPT_BUILD_ANDROID_AUDIO_SINK=ON", "-DOPT_BUILD_AUDIO_SINK=OFF", "-DOPT_BUILD_DISCORD_PRESENCE=OFF", "-DUSE_INTERNAL_LIBCORRECT=OFF" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') + } + } + + externalNativeBuild { + cmake { + version "3.18.1" + path "../../CMakeLists.txt" + } + } + + sourceSets { + main { + assets.srcDirs += ['assets'] + } + } +} + +task deleteTempAssets (type: Delete) { + delete 'assets' +} + +task copyResources(type: Copy) { + description = 'Copy resources...' + from '../../root/' + into 'assets/' + include('**/*') +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.0.2' +} + +copyResources.dependsOn deleteTempAssets +preBuild.dependsOn copyResources \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3ed585e1 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/DeviceManager.kt b/android/app/src/main/java/DeviceManager.kt new file mode 100644 index 00000000..f53f1742 --- /dev/null +++ b/android/app/src/main/java/DeviceManager.kt @@ -0,0 +1,32 @@ +package org.sdrpp.sdrpp; + +import android.app.NativeActivity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.Manifest; +import android.os.Bundle; +import android.view.View; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import android.util.Log; +import android.content.res.AssetManager; + +import androidx.core.app.ActivityCompat; + +import androidx.core.content.PermissionChecker; + +import java.util.concurrent.LinkedBlockingQueue; +import java.io.*; + +class DeviceManager { + public fun init() { + + } +} \ No newline at end of file diff --git a/android/app/src/main/java/MainActivity.kt b/android/app/src/main/java/MainActivity.kt new file mode 100644 index 00000000..9fbf5ec4 --- /dev/null +++ b/android/app/src/main/java/MainActivity.kt @@ -0,0 +1,192 @@ +package org.sdrpp.sdrpp; + +import android.app.NativeActivity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.Manifest; +import android.os.Bundle; +import android.view.View; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import android.util.Log; +import android.content.res.AssetManager; + +import androidx.core.app.ActivityCompat; + +import androidx.core.content.PermissionChecker; + +import java.util.concurrent.LinkedBlockingQueue; +import java.io.*; + +private const val ACTION_USB_PERMISSION = "org.sdrpp.sdrpp.USB_PERMISSION"; + +private val usbReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (ACTION_USB_PERMISSION == intent.action) { + synchronized(this) { + var _this = context as MainActivity; + _this.SDR_device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + _this.SDR_conn = _this.usbManager!!.openDevice(_this.SDR_device); + + // Save SDR info + _this.SDR_VID = _this.SDR_device!!.getVendorId(); + _this.SDR_PID = _this.SDR_device!!.getProductId() + _this.SDR_FD = _this.SDR_conn!!.getFileDescriptor(); + } + + // Whatever the hell this does + context.unregisterReceiver(this); + + // Hide again the system bars + _this.hideSystemBars(); + } + } + } +} + +class MainActivity : NativeActivity() { + private val TAG : String = "SDR++"; + public var usbManager : UsbManager? = null; + public var SDR_device : UsbDevice? = null; + public var SDR_conn : UsbDeviceConnection? = null; + public var SDR_VID : Int = -1; + public var SDR_PID : Int = -1; + public var SDR_FD : Int = -1; + + fun checkAndAsk(permission: String) { + if (PermissionChecker.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(permission), 1); + } + } + + public fun hideSystemBars() { + val decorView = getWindow().getDecorView(); + val uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); + } + + public override fun onCreate(savedInstanceState: Bundle?) { + // Hide bars + hideSystemBars(); + + // Ask for required permissions, without these the app cannot run. + checkAndAsk(Manifest.permission.WRITE_EXTERNAL_STORAGE); + checkAndAsk(Manifest.permission.READ_EXTERNAL_STORAGE); + + // TODO: Have the main code wait until these two permissions are available + + // Register events + usbManager = getSystemService(Context.USB_SERVICE) as UsbManager; + val permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0) + val filter = IntentFilter(ACTION_USB_PERMISSION) + registerReceiver(usbReceiver, filter) + + // Get permission for all USB devices + val devList = usbManager!!.getDeviceList(); + for ((name, dev) in devList) { + usbManager!!.requestPermission(dev, permissionIntent); + } + + // Ask for internet permission + checkAndAsk(Manifest.permission.INTERNET); + + super.onCreate(savedInstanceState) + } + + public override fun onResume() { + // Hide bars again + hideSystemBars(); + super.onResume(); + } + + fun showSoftInput() { + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; + inputMethodManager.showSoftInput(window.decorView, 0); + } + + fun hideSoftInput() { + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; + inputMethodManager.hideSoftInputFromWindow(window.decorView.windowToken, 0); + hideSystemBars(); + } + + // Queue for the Unicode characters to be polled from native code (via pollUnicodeChar()) + private var unicodeCharacterQueue: LinkedBlockingQueue = LinkedBlockingQueue() + + // We assume dispatchKeyEvent() of the NativeActivity is actually called for every + // KeyEvent and not consumed by any View before it reaches here + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN) { + unicodeCharacterQueue.offer(event.getUnicodeChar(event.metaState)) + } + return super.dispatchKeyEvent(event) + } + + fun pollUnicodeChar(): Int { + return unicodeCharacterQueue.poll() ?: 0 + } + + public fun createIfDoesntExist(path: String) { + // This is a directory, create it in the filesystem + var folder = File(path); + var success = true; + if (!folder.exists()) { + success = folder.mkdirs(); + } + if (!success) { + Log.e(TAG, "Could not create folder with path " + path); + } + } + + public fun extractDir(aman: AssetManager, local: String, rsrc: String): Int { + val flist = aman.list(rsrc); + var ecount = 0; + for (fp in flist) { + val lpath = local + "/" + fp; + val rpath = rsrc + "/" + fp; + + Log.w(TAG, "Extracting '" + rpath + "' to '" + lpath + "'"); + + // Create local path if non-existent + createIfDoesntExist(local); + + // Create if directory + val ext = extractDir(aman, lpath, rpath); + + // Extract if file + if (ext == 0) { + // This is a file, extract it + val _os = FileOutputStream(lpath); + val _is = aman.open(rpath); + val ilen = _is.available(); + var fbuf = ByteArray(ilen); + _is.read(fbuf, 0, ilen); + _os.write(fbuf); + _os.close(); + _is.close(); + } + + ecount++; + } + return ecount; + } + + public fun getAppDir(): String { + val fdir = getFilesDir().getAbsolutePath(); + + // Extract all resources to the app directory + val aman = getAssets(); + extractDir(aman, fdir + "/res", "res"); + createIfDoesntExist(fdir + "/modules"); + + return fdir; + } +} diff --git a/android/app/src/main/res/mipmap/ic_launcher.png b/android/app/src/main/res/mipmap/ic_launcher.png new file mode 100644 index 00000000..03cf6183 Binary files /dev/null and b/android/app/src/main/res/mipmap/ic_launcher.png differ diff --git a/android/app/src/main/res/xml/device_filter.xml b/android/app/src/main/res/xml/device_filter.xml new file mode 100644 index 00000000..88165de3 --- /dev/null +++ b/android/app/src/main/res/xml/device_filter.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..24f9e99a --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,22 @@ +buildscript { + ext.kotlin_version = '1.4.31' + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..2d8d1e4d --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/core/backends/android/android_backend.h b/core/backends/android/android_backend.h new file mode 100644 index 00000000..50fdf515 --- /dev/null +++ b/core/backends/android/android_backend.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include + +namespace backend { + struct DevVIDPID { + uint16_t vid; + uint16_t pid; + }; + + extern const std::vector AIRSPY_VIDPIDS; + extern const std::vector AIRSPYHF_VIDPIDS; + extern const std::vector HACKRF_VIDPIDS; + extern const std::vector RTL_SDR_VIDPIDS; + + int getDeviceFD(int& vid, int& pid, const std::vector& allowedVidPids); +} \ No newline at end of file diff --git a/core/backends/android/backend.cpp b/core/backends/android/backend.cpp new file mode 100644 index 00000000..16ca2479 --- /dev/null +++ b/core/backends/android/backend.cpp @@ -0,0 +1,490 @@ +#include +#include "android_backend.h" +#include +#include +#include "imgui.h" +#include "imgui_impl_android.h" +#include "imgui_impl_opengl3.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Credit to the ImGui android OpenGL3 example for a lot of this code! + +namespace backend { + struct android_app* app = NULL; + EGLDisplay _EglDisplay = EGL_NO_DISPLAY; + EGLSurface _EglSurface = EGL_NO_SURFACE; + EGLContext _EglContext = EGL_NO_CONTEXT; + bool _Initialized = false; + char _LogTag[] = "SDR++"; + bool initialized = false; + bool pauseRendering = false; + bool exited = false; + + // Forward declaration + int ShowSoftKeyboardInput(); + int PollUnicodeChars(); + + void doPartialInit() { + backend::init(); + style::loadFonts(options::opts.root + "/res"); // TODO: Don't hardcode, use config + icons::load(options::opts.root + "/res"); + thememenu::applyTheme(); + ImGui::GetStyle().ScaleAllSizes(style::uiScale); + gui::mainWindow.setFirstMenuRender(); + } + + void handleAppCmd(struct android_app* app, int32_t appCmd) { + switch (appCmd) { + case APP_CMD_SAVE_STATE: + spdlog::warn("APP_CMD_SAVE_STATE"); + break; + case APP_CMD_INIT_WINDOW: + spdlog::warn("APP_CMD_INIT_WINDOW"); + if (pauseRendering && !exited) { + doPartialInit(); + pauseRendering = false; + } + exited = false; + break; + case APP_CMD_TERM_WINDOW: + spdlog::warn("APP_CMD_TERM_WINDOW"); + pauseRendering = true; + backend::end(); + break; + case APP_CMD_GAINED_FOCUS: + spdlog::warn("APP_CMD_GAINED_FOCUS"); + break; + case APP_CMD_LOST_FOCUS: + spdlog::warn("APP_CMD_LOST_FOCUS"); + break; + } + } + + int32_t handleInputEvent(struct android_app* app, AInputEvent* inputEvent) { + return ImGui_ImplAndroid_HandleInputEvent(inputEvent); + } + + int aquireWindow() { + while (!app->window) { + spdlog::warn("Waiting on the shitty window thing"); std::this_thread::sleep_for(std::chrono::milliseconds(30)); + int out_events; + struct android_poll_source* out_data; + + while (ALooper_pollAll(0, NULL, &out_events, (void**)&out_data) >= 0) { + // Process one event + if (out_data != NULL) { out_data->process(app, out_data); } + + // Exit the app by returning from within the infinite loop + if (app->destroyRequested != 0) { + return -1; + } + } + } + ANativeWindow_acquire(app->window); + return 0; + } + + int init(std::string resDir) { + spdlog::warn("Backend init"); + + // Get window + aquireWindow(); + + // EGL Init + { + _EglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (_EglDisplay == EGL_NO_DISPLAY) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY"); + + if (eglInitialize(_EglDisplay, 0, 0) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglInitialize() returned with an error"); + + const EGLint egl_attributes[] = { EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE }; + EGLint num_configs = 0; + if (eglChooseConfig(_EglDisplay, egl_attributes, nullptr, 0, &num_configs) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglChooseConfig() returned with an error"); + if (num_configs == 0) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglChooseConfig() returned 0 matching config"); + + // Get the first matching config + EGLConfig egl_config; + eglChooseConfig(_EglDisplay, egl_attributes, &egl_config, 1, &num_configs); + EGLint egl_format; + eglGetConfigAttrib(_EglDisplay, egl_config, EGL_NATIVE_VISUAL_ID, &egl_format); + ANativeWindow_setBuffersGeometry(app->window, 0, 0, egl_format); + + const EGLint egl_context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + _EglContext = eglCreateContext(_EglDisplay, egl_config, EGL_NO_CONTEXT, egl_context_attributes); + + if (_EglContext == EGL_NO_CONTEXT) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglCreateContext() returned EGL_NO_CONTEXT"); + + _EglSurface = eglCreateWindowSurface(_EglDisplay, egl_config, app->window, NULL); + eglMakeCurrent(_EglDisplay, _EglSurface, _EglSurface, _EglContext); + } + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + (void)io; + + // Disable loading/saving of .ini file from disk. + // FIXME: Consider using LoadIniSettingsFromMemory() / SaveIniSettingsToMemory() to save in appropriate location for Android. + io.IniFilename = NULL; + + // Setup Platform/Renderer backends + ImGui_ImplAndroid_Init(app->window); + ImGui_ImplOpenGL3_Init("#version 300 es"); + + return 0; + } + + void beginFrame() { + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplAndroid_NewFrame(); + ImGui::NewFrame(); + } + + void render(bool vsync) { + // Rendering + ImGui::Render(); + auto dSize = ImGui::GetIO().DisplaySize; + glViewport(0, 0, dSize.x, dSize.y); + glClearColor(gui::themeManager.clearColor.x, gui::themeManager.clearColor.y, gui::themeManager.clearColor.z, gui::themeManager.clearColor.w); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + eglSwapBuffers(_EglDisplay, _EglSurface); + } + + // No screen pos to detect + void getMouseScreenPos(double& x, double& y) { x = 0; y = 0; } + void setMouseScreenPos(double x, double y) {} + + int renderLoop() { + spdlog::warn("BRUH: {0}", (void*)backend::app->window); + while (true) { + int out_events; + struct android_poll_source* out_data; + + while (ALooper_pollAll(0, NULL, &out_events, (void**)&out_data) >= 0) { + // Process one event + if (out_data != NULL) { out_data->process(app, out_data); } + + // Exit the app by returning from within the infinite loop + if (app->destroyRequested != 0) { + spdlog::warn("ASKED TO EXIT"); + exited = true; + + // Stop SDR + gui::mainWindow.setPlayState(false); + return 0; + } + } + + if (_EglDisplay == EGL_NO_DISPLAY) { continue; } + + if (!pauseRendering) { + // Initiate a new frame + ImGuiIO& io = ImGui::GetIO(); + auto dsize = io.DisplaySize; + + // Poll Unicode characters via JNI + // FIXME: do not call this every frame because of JNI overhead + PollUnicodeChars(); + + // Open on-screen (soft) input if requested by Dear ImGui + static bool WantTextInputLast = false; + if (io.WantTextInput && !WantTextInputLast) + ShowSoftKeyboardInput(); + WantTextInputLast = io.WantTextInput; + + // Render + beginFrame(); + + if (dsize.x > 0 && dsize.y > 0) { + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(dsize.x, dsize.y)); + gui::mainWindow.draw(); + } + render(); + } + else { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + } + + return 0; + } + + int end() { + // Cleanup + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplAndroid_Shutdown(); + ImGui::DestroyContext(); + + // Destroy all + if (_EglDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(_EglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (_EglContext != EGL_NO_CONTEXT) { eglDestroyContext(_EglDisplay, _EglContext); } + if (_EglSurface != EGL_NO_SURFACE) { eglDestroySurface(_EglDisplay, _EglSurface); } + eglTerminate(_EglDisplay); + } + + _EglDisplay = EGL_NO_DISPLAY; + _EglContext = EGL_NO_CONTEXT; + _EglSurface = EGL_NO_SURFACE; + + if (app->window) { ANativeWindow_release(app->window); } + + return 0; + } + + int ShowSoftKeyboardInput() { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + return -1; + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + return -2; + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + return -3; + + jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "showSoftInput", "()V"); + if (method_id == NULL) + return -4; + + java_env->CallVoidMethod(app->activity->clazz, method_id); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + return -5; + + return 0; + } + + int getDeviceFD(int& vid, int& pid, const std::vector& allowedVidPids) { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + return -1; + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + return -1; + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + return -1; + + jfieldID fd_field_id = java_env->GetFieldID(native_activity_clazz, "SDR_FD", "I"); + jfieldID vid_field_id = java_env->GetFieldID(native_activity_clazz, "SDR_VID", "I"); + jfieldID pid_field_id = java_env->GetFieldID(native_activity_clazz, "SDR_PID", "I"); + + if (!vid_field_id || !vid_field_id || !pid_field_id) + return -1; + + int fd = java_env->GetIntField(app->activity->clazz, fd_field_id); + vid = java_env->GetIntField(app->activity->clazz, vid_field_id); + pid = java_env->GetIntField(app->activity->clazz, pid_field_id); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + return -1; + + // If no vid/pid was given, just return successfully + if (allowedVidPids.empty()) { + return fd; + } + + // Otherwise, check that the vid/pid combo is allowed + for (auto const& vp : allowedVidPids) { + if (vp.vid != vid || vp.pid != pid) { continue; } + return fd; + } + + return -1; + } + + // Unfortunately, the native KeyEvent implementation has no getUnicodeChar() function. + // Therefore, we implement the processing of KeyEvents in MainActivity.kt and poll + // the resulting Unicode characters here via JNI and send them to Dear ImGui. + int PollUnicodeChars() { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + return -1; + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + return -2; + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + return -3; + + jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "pollUnicodeChar", "()I"); + if (method_id == NULL) + return -4; + + // Send the actual characters to Dear ImGui + ImGuiIO& io = ImGui::GetIO(); + jint unicode_character; + while ((unicode_character = java_env->CallIntMethod(app->activity->clazz, method_id)) != 0) + io.AddInputCharacter(unicode_character); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + return -5; + + return 0; + } + + std::string getAppFilesDir() { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + throw std::runtime_error("Could not get JNI environement"); + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + throw std::runtime_error("Could not attach to thread"); + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + throw std::runtime_error("Could not get MainActivity class"); + + jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "getAppDir", "()Ljava/lang/String;"); + if (method_id == NULL) + throw std::runtime_error("Could not get methode ID"); + + jstring jstr = (jstring)java_env->CallObjectMethod(app->activity->clazz, method_id); + + const char* _str = java_env->GetStringUTFChars(jstr, NULL); + std::string str(_str); + java_env->ReleaseStringUTFChars(jstr, _str); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + throw std::runtime_error("Could not detach from thread"); + + + return str; + } + + const std::vector AIRSPY_VIDPIDS = { + { 0x1d50, 0x60a1 } + }; + + const std::vector AIRSPYHF_VIDPIDS = { + { 0x03EB, 0x800C } + }; + + const std::vector HACKRF_VIDPIDS = { + { 0x1d50, 0x604b }, + { 0x1d50, 0x6089 }, + { 0x1d50, 0xcc15 } + }; + + const std::vector RTL_SDR_VIDPIDS = { + { 0x0bda, 0x2832 }, + { 0x0bda, 0x2838 }, + { 0x0413, 0x6680 }, + { 0x0413, 0x6f0f }, + { 0x0458, 0x707f }, + { 0x0ccd, 0x00a9 }, + { 0x0ccd, 0x00b3 }, + { 0x0ccd, 0x00b4 }, + { 0x0ccd, 0x00b5 }, + { 0x0ccd, 0x00b7 }, + { 0x0ccd, 0x00b8 }, + { 0x0ccd, 0x00b9 }, + { 0x0ccd, 0x00c0 }, + { 0x0ccd, 0x00c6 }, + { 0x0ccd, 0x00d3 }, + { 0x0ccd, 0x00d7 }, + { 0x0ccd, 0x00e0 }, + { 0x1554, 0x5020 }, + { 0x15f4, 0x0131 }, + { 0x15f4, 0x0133 }, + { 0x185b, 0x0620 }, + { 0x185b, 0x0650 }, + { 0x185b, 0x0680 }, + { 0x1b80, 0xd393 }, + { 0x1b80, 0xd394 }, + { 0x1b80, 0xd395 }, + { 0x1b80, 0xd397 }, + { 0x1b80, 0xd398 }, + { 0x1b80, 0xd39d }, + { 0x1b80, 0xd3a4 }, + { 0x1b80, 0xd3a8 }, + { 0x1b80, 0xd3af }, + { 0x1b80, 0xd3b0 }, + { 0x1d19, 0x1101 }, + { 0x1d19, 0x1102 }, + { 0x1d19, 0x1103 }, + { 0x1d19, 0x1104 }, + { 0x1f4d, 0xa803 }, + { 0x1f4d, 0xb803 }, + { 0x1f4d, 0xc803 }, + { 0x1f4d, 0xd286 }, + { 0x1f4d, 0xd803 } + }; +} + +extern "C" { + void android_main(struct android_app* app) { + // Save app instance + app->onAppCmd = backend::handleAppCmd; + app->onInputEvent = backend::handleInputEvent; + backend::app = app; + + // Check if this is the first time we run or not + if (backend::initialized) { + spdlog::warn("android_main called again"); + backend::doPartialInit(); + backend::pauseRendering = false; + backend::renderLoop(); + return; + } + backend::initialized = true; + + // prepare spdlog + auto console_sink = std::make_shared("SDR++"); + auto logger = std::shared_ptr(new spdlog::logger("", { console_sink })); + spdlog::set_default_logger(logger); + + // Grab files dir + std::string appdir = backend::getAppFilesDir(); + + // Call main + char* rootpath = new char[appdir.size() + 1]; + strcpy(rootpath, appdir.c_str()); + char* dummy[] = { "", "-r", rootpath }; + sdrpp_main(3, dummy); + } +} \ No newline at end of file diff --git a/core/backends/android/imgui/imgui_impl_android.cpp b/core/backends/android/imgui/imgui_impl_android.cpp new file mode 100644 index 00000000..3201bee2 --- /dev/null +++ b/core/backends/android/imgui/imgui_impl_android.cpp @@ -0,0 +1,187 @@ +// dear imgui: Platform Binding for Android native app +// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) + +// Implemented features: +// [X] Platform: Keyboard arrays indexed using AKEYCODE_* codes, e.g. ImGui::IsKeyPressed(AKEYCODE_SPACE). +// Missing features: +// [ ] Platform: Clipboard support. +// [ ] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. +// [ ] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. +// Important: +// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) +// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. +// Read online: https://github.com/ocornut/imgui/tree/master/docs + +// CHANGELOG +// (minor and older changes stripped away, please see git history for details) +// 2021-03-04: Initial version. + +#include "imgui.h" +#include "imgui_impl_android.h" +#include +#include +#include +#include +#include +#include +#include + +// Android data +static double g_Time = 0.0; +static ANativeWindow* g_Window; +static char g_LogTag[] = "ImGuiExample"; +static std::map> g_KeyEventQueues; // FIXME: Remove dependency on map and queue once we use upcoming input queue. + +int32_t ImGui_ImplAndroid_HandleInputEvent(AInputEvent* input_event) +{ + ImGuiIO& io = ImGui::GetIO(); + int32_t event_type = AInputEvent_getType(input_event); + switch (event_type) + { + case AINPUT_EVENT_TYPE_KEY: + { + int32_t event_key_code = AKeyEvent_getKeyCode(input_event); + int32_t event_action = AKeyEvent_getAction(input_event); + int32_t event_meta_state = AKeyEvent_getMetaState(input_event); + + io.KeyCtrl = ((event_meta_state & AMETA_CTRL_ON) != 0); + io.KeyShift = ((event_meta_state & AMETA_SHIFT_ON) != 0); + io.KeyAlt = ((event_meta_state & AMETA_ALT_ON) != 0); + + switch (event_action) + { + // FIXME: AKEY_EVENT_ACTION_DOWN and AKEY_EVENT_ACTION_UP occur at once as soon as a touch pointer + // goes up from a key. We use a simple key event queue/ and process one event per key per frame in + // ImGui_ImplAndroid_NewFrame()...or consider using IO queue, if suitable: https://github.com/ocornut/imgui/issues/2787 + case AKEY_EVENT_ACTION_DOWN: + case AKEY_EVENT_ACTION_UP: + g_KeyEventQueues[event_key_code].push(event_action); + break; + default: + break; + } + break; + } + case AINPUT_EVENT_TYPE_MOTION: + { + int32_t event_action = AMotionEvent_getAction(input_event); + int32_t event_pointer_index = (event_action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; + event_action &= AMOTION_EVENT_ACTION_MASK; + switch (event_action) + { + case AMOTION_EVENT_ACTION_DOWN: + case AMOTION_EVENT_ACTION_UP: + // Physical mouse buttons (and probably other physical devices) also invoke the actions AMOTION_EVENT_ACTION_DOWN/_UP, + // but we have to process them separately to identify the actual button pressed. This is done below via + // AMOTION_EVENT_ACTION_BUTTON_PRESS/_RELEASE. Here, we only process "FINGER" input (and "UNKNOWN", as a fallback). + if((AMotionEvent_getToolType(input_event, event_pointer_index) == AMOTION_EVENT_TOOL_TYPE_FINGER) + || (AMotionEvent_getToolType(input_event, event_pointer_index) == AMOTION_EVENT_TOOL_TYPE_STYLUS) || (AMotionEvent_getToolType(input_event, event_pointer_index) == AMOTION_EVENT_TOOL_TYPE_UNKNOWN)) + { + io.MouseDown[0] = (event_action == AMOTION_EVENT_ACTION_DOWN); + io.MousePos = ImVec2(AMotionEvent_getX(input_event, event_pointer_index), AMotionEvent_getY(input_event, event_pointer_index)); + } + break; + case AMOTION_EVENT_ACTION_BUTTON_PRESS: + case AMOTION_EVENT_ACTION_BUTTON_RELEASE: + { + int32_t button_state = AMotionEvent_getButtonState(input_event); + io.MouseDown[0] = ((button_state & AMOTION_EVENT_BUTTON_PRIMARY) != 0); + io.MouseDown[1] = ((button_state & AMOTION_EVENT_BUTTON_SECONDARY) != 0); + io.MouseDown[2] = ((button_state & AMOTION_EVENT_BUTTON_TERTIARY) != 0); + } + break; + case AMOTION_EVENT_ACTION_HOVER_MOVE: // Hovering: Tool moves while NOT pressed (such as a physical mouse) + case AMOTION_EVENT_ACTION_MOVE: // Touch pointer moves while DOWN + io.MousePos = ImVec2(AMotionEvent_getX(input_event, event_pointer_index), AMotionEvent_getY(input_event, event_pointer_index)); + break; + case AMOTION_EVENT_ACTION_SCROLL: + io.MouseWheel = AMotionEvent_getAxisValue(input_event, AMOTION_EVENT_AXIS_VSCROLL, event_pointer_index); + io.MouseWheelH = AMotionEvent_getAxisValue(input_event, AMOTION_EVENT_AXIS_HSCROLL, event_pointer_index); + break; + default: + break; + } + } + return 1; + default: + break; + } + + return 0; +} + +bool ImGui_ImplAndroid_Init(ANativeWindow* window) +{ + g_Window = window; + g_Time = 0.0; + + // Setup backend capabilities flags + ImGuiIO& io = ImGui::GetIO(); + io.BackendPlatformName = "imgui_impl_android"; + + // Keyboard mapping. Dear ImGui will use those indices to peek into the io.KeysDown[] array. + io.KeyMap[ImGuiKey_Tab] = AKEYCODE_TAB; + io.KeyMap[ImGuiKey_LeftArrow] = AKEYCODE_DPAD_LEFT; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_RightArrow] = AKEYCODE_DPAD_RIGHT; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_UpArrow] = AKEYCODE_DPAD_UP; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_DownArrow] = AKEYCODE_DPAD_DOWN; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_PageUp] = AKEYCODE_PAGE_UP; + io.KeyMap[ImGuiKey_PageDown] = AKEYCODE_PAGE_DOWN; + io.KeyMap[ImGuiKey_Home] = AKEYCODE_MOVE_HOME; + io.KeyMap[ImGuiKey_End] = AKEYCODE_MOVE_END; + io.KeyMap[ImGuiKey_Insert] = AKEYCODE_INSERT; + io.KeyMap[ImGuiKey_Delete] = AKEYCODE_FORWARD_DEL; + io.KeyMap[ImGuiKey_Backspace] = AKEYCODE_DEL; + io.KeyMap[ImGuiKey_Space] = AKEYCODE_SPACE; + io.KeyMap[ImGuiKey_Enter] = AKEYCODE_ENTER; + io.KeyMap[ImGuiKey_Escape] = AKEYCODE_ESCAPE; + io.KeyMap[ImGuiKey_KeyPadEnter] = AKEYCODE_NUMPAD_ENTER; + io.KeyMap[ImGuiKey_A] = AKEYCODE_A; + io.KeyMap[ImGuiKey_C] = AKEYCODE_C; + io.KeyMap[ImGuiKey_V] = AKEYCODE_V; + io.KeyMap[ImGuiKey_X] = AKEYCODE_X; + io.KeyMap[ImGuiKey_Y] = AKEYCODE_Y; + io.KeyMap[ImGuiKey_Z] = AKEYCODE_Z; + + return true; +} + +void ImGui_ImplAndroid_Shutdown() +{ +} + +void ImGui_ImplAndroid_NewFrame() +{ + ImGuiIO& io = ImGui::GetIO(); + + // Process queued key events + // FIXME: This is a workaround for multiple key event actions occurring at once (see above) and can be removed once we use upcoming input queue. + for (auto& key_queue : g_KeyEventQueues) + { + if (key_queue.second.empty()) + continue; + io.KeysDown[key_queue.first] = (key_queue.second.front() == AKEY_EVENT_ACTION_DOWN); + key_queue.second.pop(); + } + + // Setup display size (every frame to accommodate for window resizing) + int32_t window_width = ANativeWindow_getWidth(g_Window); + int32_t window_height = ANativeWindow_getHeight(g_Window); + int display_width = window_width; + int display_height = window_height; + + io.DisplaySize = ImVec2((float)window_width, (float)window_height); + if (window_width > 0 && window_height > 0) + io.DisplayFramebufferScale = ImVec2((float)display_width / window_width, (float)display_height / window_height); + + // Setup time step + struct timespec current_timespec; + clock_gettime(CLOCK_MONOTONIC, ¤t_timespec); + double current_time = (double)(current_timespec.tv_sec) + (current_timespec.tv_nsec / 1000000000.0); + io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f / 60.0f); + g_Time = current_time; +} diff --git a/core/backends/android/imgui/imgui_impl_android.h b/core/backends/android/imgui/imgui_impl_android.h new file mode 100644 index 00000000..92b466b6 --- /dev/null +++ b/core/backends/android/imgui/imgui_impl_android.h @@ -0,0 +1,27 @@ +// dear imgui: Platform Binding for Android native app +// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) + +// Implemented features: +// [X] Platform: Keyboard arrays indexed using AKEYCODE_* codes, e.g. ImGui::IsKeyPressed(AKEYCODE_SPACE). +// Missing features: +// [ ] Platform: Clipboard support. +// [ ] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. +// [ ] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. +// Important: +// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) +// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. +// Read online: https://github.com/ocornut/imgui/tree/master/docs + +#pragma once + +struct ANativeWindow; +struct AInputEvent; + +IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window); +IMGUI_IMPL_API int32_t ImGui_ImplAndroid_HandleInputEvent(AInputEvent* input_event); +IMGUI_IMPL_API void ImGui_ImplAndroid_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplAndroid_NewFrame(); diff --git a/core/backends/android/keybinds.h b/core/backends/android/keybinds.h new file mode 100644 index 00000000..83bc6a66 --- /dev/null +++ b/core/backends/android/keybinds.h @@ -0,0 +1,27 @@ +#pragma once + +// No keyboard controls +#define KB_KEY_HOME -1 +#define KB_KEY_MENU -1 +#define KB_KEY_END -1 +#define KB_KEY_ESC -1 +#define KB_KEY_PG_UP -1 +#define KB_KEY_PG_DOWN -1 + +#define KB_KEY_ENTER -1 +#define KB_KEY_KP_ENTER -1 +#define KB_KEY_DEL -1 +#define KB_KEY_BACKSPACE -1 + +#define KB_KEY_LEFT -1 +#define KB_KEY_RIGHT -1 +#define KB_KEY_UP -1 +#define KB_KEY_DOWN -1 + +#define KB_KEY_LCTRL -1 +#define KB_KEY_RCTRL -1 +#define KB_KEY_LSHIFT -1 +#define KB_KEY_RSHIFT -1 + +#define KB_KEY_A -1 +#define KB_KEY_R -1 \ No newline at end of file diff --git a/sink_modules/android_audio_sink/CMakeLists.txt b/sink_modules/android_audio_sink/CMakeLists.txt new file mode 100644 index 00000000..92b2a473 --- /dev/null +++ b/sink_modules/android_audio_sink/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.13) +project(audio_sink) + +file(GLOB SRC "src/*.cpp") + +add_library(audio_sink SHARED ${SRC}) +target_link_libraries(audio_sink PRIVATE sdrpp_core) +set_target_properties(audio_sink PROPERTIES PREFIX "") + +target_include_directories(audio_sink PRIVATE "src/") + +target_compile_options(audio_sink PRIVATE -O3 -std=c++17) +target_link_libraries(audio_sink PRIVATE aaudio) \ No newline at end of file diff --git a/sink_modules/android_audio_sink/src/main.cpp b/sink_modules/android_audio_sink/src/main.cpp new file mode 100644 index 00000000..a4837dd2 --- /dev/null +++ b/sink_modules/android_audio_sink/src/main.cpp @@ -0,0 +1,195 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +SDRPP_MOD_INFO{ + /* Name: */ "audio_sink", + /* Description: */ "Android audio sink module for SDR++", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ 1 +}; + +ConfigManager config; + +class AudioSink : SinkManager::Sink { +public: + AudioSink(SinkManager::Stream* stream, std::string streamName) { + _stream = stream; + _streamName = streamName; + + packer.init(_stream->sinkOut, 512); + + // TODO: Add choice? I don't think anyone cares on android... + sampleRate = 48000; + _stream->setSampleRate(sampleRate); + } + + ~AudioSink() { + } + + void start() { + if (running) { + return; + } + doStart(); + running = true; + } + + void stop() { + if (!running) { + return; + } + doStop(); + running = false; + } + + void menuHandler() { + // Draw menu here + } + +private: + void doStart() { + // Create stream builder + AAudioStreamBuilder *builder; + aaudio_result_t result = AAudio_createStreamBuilder(&builder); + + // Set stream options + bufferSize = round(sampleRate / 60.0); + AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT); + AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED); + AAudioStreamBuilder_setSampleRate(builder, sampleRate); + AAudioStreamBuilder_setChannelCount(builder, 2); + AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT); + AAudioStreamBuilder_setBufferCapacityInFrames(builder, bufferSize); + AAudioStreamBuilder_setErrorCallback(builder, errorCallback, this); + packer.setSampleCount(bufferSize); + + // Open the stream + AAudioStreamBuilder_openStream(builder, &stream); + + // Stream stream and packer + packer.start(); + AAudioStream_requestStart(stream); + + // We no longer need the builder + AAudioStreamBuilder_delete(builder); + + // Start worker thread + workerThread = std::thread(&AudioSink::worker, this); + } + + void doStop() { + packer.stop(); + packer.out.stopReader(); + AAudioStream_requestStop(stream); + AAudioStream_close(stream); + if (workerThread.joinable()) { workerThread.join(); } + packer.out.clearReadStop(); + } + + void worker() { + while (true) { + int count = packer.out.read(); + if (count < 0) { return; } + AAudioStream_write(stream, packer.out.readBuf, count, 100000000); + packer.out.flush(); + } + } + + static void errorCallback(AAudioStream *stream, void *userData, aaudio_result_t error){ + // detect an audio device detached and restart the stream + if (error == AAUDIO_ERROR_DISCONNECTED){ + std::thread thr(&AudioSink::restart, (AudioSink*)userData); + thr.detach(); + } + } + + void restart() { + if (running) { doStop(); } + if (running) { doStart(); } + } + + std::thread workerThread; + + AAudioStream *stream = NULL; + SinkManager::Stream* _stream; + dsp::Packer packer; + + std::string _streamName; + double sampleRate; + int bufferSize; + + bool running = false; +}; + +class AudioSinkModule : public ModuleManager::Instance { +public: + AudioSinkModule(std::string name) { + this->name = name; + provider.create = create_sink; + provider.ctx = this; + + sigpath::sinkManager.registerSinkProvider("Audio", provider); + } + + ~AudioSinkModule() { + // Unregister sink, this will automatically stop and delete all instances of the audio sink + sigpath::sinkManager.unregisterSinkProvider("Audio"); + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + +private: + static SinkManager::Sink* create_sink(SinkManager::Stream* stream, std::string streamName, void* ctx) { + return (SinkManager::Sink*)(new AudioSink(stream, streamName)); + } + + std::string name; + bool enabled = true; + SinkManager::SinkProvider provider; +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + config.setPath(options::opts.root + "/audio_sink_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT void* _CREATE_INSTANCE_(std::string name) { + AudioSinkModule* instance = new AudioSinkModule(name); + return instance; +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (AudioSinkModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file