diff --git a/universal7885-common/apps/FMRadio/Android.bp b/universal7885-common/apps/FMRadio/Android.bp
new file mode 100644
index 0000000..132aa56
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/Android.bp
@@ -0,0 +1,21 @@
+android_app {
+ name: "FMRadio",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ platform_apis: true,
+ privileged: true,
+ certificate: "platform",
+ jni_libs: ["libfmioctl_jni"],
+ static_libs: [
+ "androidx.core_core",
+ "androidx.appcompat_appcompat",
+ ],
+ required: ["privapp_whitelist_com.eurekateam.fmradio.xml"],
+}
+
+prebuilt_etc {
+ name: "privapp_whitelist_com.eurekateam.fmradio.xml",
+ src: "privapp_whitelist_com.eurekateam.fmradio.xml",
+ sub_dir: "permissions",
+}
diff --git a/universal7885-common/apps/FMRadio/AndroidManifest.xml b/universal7885-common/apps/FMRadio/AndroidManifest.xml
new file mode 100644
index 0000000..e7337e2
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/AndroidManifest.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/universal7885-common/apps/FMRadio/jni/Android.bp b/universal7885-common/apps/FMRadio/jni/Android.bp
new file mode 100644
index 0000000..560edd8
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/jni/Android.bp
@@ -0,0 +1,12 @@
+cc_library_shared {
+ name: "libfmioctl_jni",
+ cflags: [
+ "-Wall",
+ "-Wextra",
+ "-Wno-unused-parameter",
+ "-Werror",
+ ],
+ srcs: ["fm_ioctl.cpp"],
+ header_libs: ["jni_headers"],
+}
+
diff --git a/universal7885-common/apps/FMRadio/jni/fm.h b/universal7885-common/apps/FMRadio/jni/fm.h
new file mode 100644
index 0000000..f84660e
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/jni/fm.h
@@ -0,0 +1,65 @@
+#define FM_FAILURE -1
+#define FM_SUCCESS 0
+
+#define V4L2_CID_USER_S610_BASE (0x00980900 + 0x1070)
+enum s610_ctrl_id {
+ V4L2_CID_S610_CH_SPACING = (V4L2_CID_USER_S610_BASE + 0x01),
+ V4L2_CID_S610_CH_BAND = (V4L2_CID_USER_S610_BASE + 0x02),
+ V4L2_CID_S610_SOFT_STEREO_BLEND = (V4L2_CID_USER_S610_BASE + 0x03),
+ V4L2_CID_S610_SOFT_STEREO_BLEND_COEFF = (V4L2_CID_USER_S610_BASE+0x04),
+ V4L2_CID_S610_SOFT_MUTE_COEFF = (V4L2_CID_USER_S610_BASE + 0x5),
+ V4L2_CID_S610_RSSI_CURR = (V4L2_CID_USER_S610_BASE + 0x06),
+ V4L2_CID_S610_SNR_CURR = (V4L2_CID_USER_S610_BASE + 0x07),
+ V4L2_CID_S610_SEEK_CANCEL = (V4L2_CID_USER_S610_BASE + 0x08),
+ V4L2_CID_S610_SEEK_MODE = (V4L2_CID_USER_S610_BASE + 0x09),
+ V4L2_CID_S610_RDS_ON = (V4L2_CID_USER_S610_BASE + 0x0A),
+ V4L2_CID_S610_IF_COUNT1 = (V4L2_CID_USER_S610_BASE + 0x0B),
+ V4L2_CID_S610_IF_COUNT2 = (V4L2_CID_USER_S610_BASE + 0x0C),
+ V4L2_CID_S610_RSSI_TH = (V4L2_CID_USER_S610_BASE + 0x0D),
+ V4L2_CID_S610_KERNEL_VER = (V4L2_CID_USER_S610_BASE + 0x0E),
+ V4L2_CID_S610_SOFT_STEREO_BLEND_REF = (V4L2_CID_USER_S610_BASE+0x0F),
+ V4L2_CID_S610_REG_RW_ADDR = (V4L2_CID_USER_S610_BASE + 0x10),
+ V4L2_CID_S610_REG_RW = (V4L2_CID_USER_S610_BASE + 0x11),
+};
+
+/* Tunner modes */
+enum fm_tuner_mode {
+ FM_TUNER_STOP_SEARCH_MODE = 0,
+ FM_TUNER_PRESET_MODE = 1,
+ FM_TUNER_AUTONOMOUS_SEARCH_MODE = 2,
+ FM_TUNER_AUTONOMOUS_SEARCH_MODE_NEXT = 10
+};
+
+/* channel spacing */
+enum fm_channel_spacing {
+ FM_CHANNEL_SPACING_50KHZ = 1,
+ FM_CHANNEL_SPACING_100KHZ = 2,
+ FM_CHANNEL_SPACING_200KHZ = 4
+};
+
+/* Mute modes */
+enum fm_mute_mode {
+ FM_MUTE_ON = 0,
+ FM_MUTE_OFF = 1,
+ FM_MUTE_ATTENUATE = 2
+};
+
+/* FM RDS modes */
+enum fm_rds_mode {
+ FM_RDS_DISABLE = 0,
+ FM_RDS_ENABLE = 1
+};
+
+#define FM_RADIO_RDS_DATA_MAX 48
+
+enum s610_freq_bands {
+ S610_BAND_FM = 0,
+ S610_BAND_AM = 1,
+};
+
+enum s610_aud_mode {
+ S610_AUD_ENABLE = 1,
+ S610_AUD_DISABLE = 0,
+};
+
+
diff --git a/universal7885-common/apps/FMRadio/jni/fm_ioctl.cpp b/universal7885-common/apps/FMRadio/jni/fm_ioctl.cpp
new file mode 100644
index 0000000..fbd7f6e
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/jni/fm_ioctl.cpp
@@ -0,0 +1,484 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "fm.h"
+#include "kernel_internal.h"
+#include
+#include
+
+//#define DEBUG
+#define TRACK_SIZE 30
+long tracks[TRACK_SIZE] = { 0 };
+bool FMThread = false;
+
+int open_fm_device(){
+ int fd;
+ if ((fd = open("/dev/radio0", O_RDWR)) < 0){
+ printf("Cannot open /dev/radio0.\n");
+ return -1;
+ }
+ return fd;
+}
+static int fm_radio_get_frequency(int fd, long *channel)
+{
+ struct v4l2_frequency freq{};
+ int ret;
+
+ freq.tuner = 0;
+ freq.type = V4L2_TUNER_RADIO;
+
+ ret = ioctl(fd, VIDIOC_G_FREQUENCY, &freq);
+ if (ret < 0) {
+ printf("FmRadioController: failed to get frequency\n");
+ return FM_FAILURE;
+ }
+
+ *channel = (long)freq.frequency/16000;
+
+ return FM_SUCCESS;
+}
+
+static int fm_radio_set_frequency(int fd, long channel)
+{
+ struct v4l2_frequency freq{};
+ int ret;
+
+ freq.tuner = 0;
+ freq.type = V4L2_TUNER_RADIO;
+ freq.frequency = (unsigned int) channel * 16000;
+
+ ret = ioctl(fd, VIDIOC_S_FREQUENCY, &freq);
+ if (ret < 0) {
+ printf("FmRadioController: failed to set frequency\n");
+ return FM_FAILURE;
+ }
+
+ return FM_SUCCESS;
+}
+
+static int fm_radio_get_control(int fd, unsigned int id, long *val)
+{
+ struct v4l2_control ctrl{};
+ int ret;
+#ifdef DEBUG
+ printf("FmRadioController:fm_radio_get_control: id(%d)\n", id);
+#endif
+ ctrl.id = id;
+
+ ret = ioctl(fd, VIDIOC_G_CTRL, &ctrl);
+ if (ret < 0) {
+ printf("FmRadioController: failed to get control\n");
+ return FM_FAILURE;
+ }
+
+ *val = (long)ctrl.value;
+
+ return FM_SUCCESS;
+}
+
+static int fm_radio_set_control(int fd, unsigned int id, long val)
+{
+ struct v4l2_control ctrl{};
+ int ret;
+#ifdef DEBUG
+ printf("FmRadioController:fm_radio_set_control: id(%d) val(%ld)\n", id, val);
+#endif
+ ctrl.id = id;
+ if (val)
+ ctrl.value = (unsigned int)val;
+ else
+ ctrl.value = 0;
+
+ ret = ioctl(fd, VIDIOC_S_CTRL, &ctrl);
+ if (ret < 0) {
+ printf("FmRadioController: failed to set control\n");
+ return FM_FAILURE;
+ }
+
+ return FM_SUCCESS;
+}
+
+static int fm_radio_seek_frequency(int fd, unsigned int upward, unsigned int wrap_around, unsigned int spacing)
+{
+ struct v4l2_hw_freq_seek seek{};
+ int ret;
+
+ seek.tuner = 0;
+ seek.type = V4L2_TUNER_RADIO;
+ seek.seek_upward = upward;
+ seek.wrap_around = wrap_around;
+ seek.spacing = spacing;
+
+ ret = ioctl(fd, VIDIOC_S_HW_FREQ_SEEK, &seek);
+ if (ret < 0) {
+ printf("FmRadioController: failed to seek frequency\n");
+ return FM_FAILURE;
+ }
+
+ return FM_SUCCESS;
+}
+static int fm_radio_channel_searching(int fd, unsigned int upward, unsigned int wrap_around, unsigned int spacing, long *channel)
+{
+ int ret;
+
+ ret = fm_radio_set_control(fd, V4L2_CID_S610_SEEK_MODE, FM_TUNER_AUTONOMOUS_SEARCH_MODE);
+ if (ret < 0)
+ return ret;
+
+ ret = fm_radio_seek_frequency(fd, upward, wrap_around, spacing);
+ if (ret < 0)
+ return ret;
+
+ ret = fm_radio_get_frequency(fd, channel);
+ if (ret < 0)
+ return ret;
+
+ return ret;
+}
+static int fm_radio_set_tuner(int fd, unsigned int mode)
+{
+ struct v4l2_tuner tuner{};
+ int ret;
+
+ tuner.index = 0;
+ tuner.audmode = mode;
+ tuner.type = 1;
+
+ ret = ioctl(fd, VIDIOC_S_TUNER, &tuner);
+ if (ret < 0) {
+ printf("FmRadioController: failed to set tuner\n");
+ return FM_FAILURE;
+ }
+
+ return FM_SUCCESS;
+}
+static int fm_radio_get_tuner(int fd)
+{
+ struct v4l2_tuner tuner{};
+ int ret;
+
+ tuner.index = 0;
+ tuner.type = 1;
+ ret = ioctl(fd, VIDIOC_G_TUNER, &tuner);
+ if (ret < 0) {
+ printf("FmRadioController: failed to set tuner\n");
+ return FM_FAILURE;
+ }
+
+ return tuner.audmode;
+}
+
+static int fm_radio_set_mute(int fd, bool mute)
+{
+ int muteint = 1;
+ if (mute) muteint = 0;
+ int ret = fm_radio_set_control(fd, V4L2_CID_AUDIO_MUTE, muteint);
+ if (ret < 0) {
+ printf("FmRadioController: failed to set mute\n");
+ return FM_FAILURE;
+ }
+
+ return FM_SUCCESS;
+}
+
+static int fm_radio_set_volume(int fd, int volume /* 1 ~ 15 */)
+{
+ int ret = fm_radio_set_control(fd, V4L2_CID_AUDIO_VOLUME, volume);
+ if (ret < 0) {
+ printf("FmRadioController: failed to set volume\n");
+ return FM_FAILURE;
+ }
+
+ return FM_SUCCESS;
+}
+
+template
+bool contains(C&& c, T e) { return std::find(std::begin(c), std::end(c), e) != std::end(c); }
+
+static long fm_radio_get_freqs(int fd){
+ long ret = 0;
+ fm_radio_set_mute(fd, true);
+ for (long & track : tracks){
+ fm_radio_channel_searching(fd, 1, 0, FM_CHANNEL_SPACING_50KHZ, &ret);
+ if (contains(tracks, ret)) break;
+ track = ret;
+ printf("Found Freq %ld\n", ret);
+ }
+ fm_radio_set_mute(fd, false);
+ return ret;
+}
+
+static int fm_radio_poll(int fd, struct pollfd *poll_fd)
+{
+ int ret;
+
+ poll_fd->fd = fd;
+ poll_fd->events = POLLIN;
+ poll_fd->revents = 0;
+
+ ret = poll(poll_fd, 1, 360);
+ if (ret > 0) {
+ if (poll_fd->revents & POLLIN) {
+ printf("FmRadioController: ready to read\n");
+ return FM_SUCCESS;
+ }
+
+ printf("FmRadioController: cannot read yet\n");
+ return FM_FAILURE;
+ }
+
+ if (!ret) {
+ printf("FmRadioController: polling timeout\n");
+ return FM_FAILURE;
+ }
+
+ printf("FmRadioController: pollig fail: %d\n", ret);
+ return FM_FAILURE - 1;
+}
+
+static int fm_radio_read(int fd, unsigned char *buf)
+{
+ int ret;
+
+ ret = read(fd, buf, FM_RADIO_RDS_DATA_MAX);
+ if (ret < 0) {
+ printf("FmRadioController: failed to read\n");
+ return FM_FAILURE;
+ }
+
+ return ret;
+}
+
+static int fm_radio_thread(int fd)
+{
+ struct pollfd radio_poll{};
+ unsigned char read_buf[FM_RADIO_RDS_DATA_MAX];
+ int ret;
+
+ while (FMThread) {
+ ret = fm_radio_poll(fd, &radio_poll);
+ if (ret < 0) {
+ if (ret < FM_FAILURE)
+ break;
+ else
+ continue;
+ }
+
+ ret = fm_radio_read(fd, read_buf);
+ if (ret < 0)
+ break;
+ }
+ return 0;
+}
+static unsigned int fm_radio_get_upperband_limit(int fd)
+{
+ int ret;
+ struct v4l2_tuner tuner{};
+ unsigned int freq;
+ tuner.index = 0;
+ ret = ioctl(fd, VIDIOC_G_TUNER, &tuner);
+ if(ret < 0) {
+ return FM_FAILURE;
+ }else {
+ freq = (tuner.rangehigh / 16000);
+ return freq;
+ }
+}
+
+static unsigned int fm_radio_get_lowerband_limit(int fd)
+{
+ int ret;
+ unsigned int freq;
+ struct v4l2_tuner tuner{};
+
+ tuner.index = 0;
+ ret = ioctl(fd, VIDIOC_G_TUNER, &tuner);
+ if(ret < 0) {
+ return FM_FAILURE;
+ }else {
+ freq = (tuner.rangelow / 16000);
+ return freq;
+ }
+}
+static long fm_radio_get_rmssi(int fd)
+{
+ struct v4l2_tuner tuner{};
+ int ret;
+ long rmssi;
+ tuner.index = 0;
+ tuner.signal = 0;
+ ret = ioctl(fd, VIDIOC_G_TUNER, &tuner);
+ if(ret < 0) {
+ ret = FM_FAILURE;
+ }else {
+ rmssi = tuner.signal;
+ ret = rmssi;
+ }
+ return ret;
+}
+static int fm_radio_set_rssi(int fd, long rssi){
+ int ret = fm_radio_set_control(fd, V4L2_CID_S610_RSSI_TH, rssi);
+ if (ret < 0){
+ return FM_FAILURE;
+ }
+ return FM_SUCCESS;
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_openFMDevice
+(__unused JNIEnv *env, __unused jobject thiz) {
+ return open_fm_device();
+}
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getFMFreq
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ long freq;
+ fm_radio_get_frequency(fd, &freq);
+ return freq;
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMFreq
+(__unused JNIEnv *env, __unused jobject thiz, jint fd, jint freq) {
+ return fm_radio_set_frequency(fd, freq);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMVolume
+(__unused JNIEnv *env, __unused jobject thiz, jint fd, jint volume) {
+ return fm_radio_set_volume(fd, volume);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMMute
+(__unused JNIEnv *env, __unused jobject thiz, jint fd, jboolean mute) {
+ return fm_radio_set_mute(fd, mute);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getFmUpper
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ return fm_radio_get_upperband_limit(fd);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getFMLower
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ return fm_radio_get_lowerband_limit(fd);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getRMSSI
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ return fm_radio_get_rmssi(fd);
+}
+extern "C"
+JNIEXPORT jlongArray JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getFMTracks
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ fm_radio_get_freqs(fd);
+ jlongArray result;
+ result = (*env).NewLongArray(TRACK_SIZE);
+ if (result == nullptr) {
+ return nullptr; /* out of memory error thrown */
+ }
+ int i;
+ // fill a temp structure to use to populate the java int array
+ jlong fill[TRACK_SIZE];
+ for (i = 0; i < TRACK_SIZE; i++) {
+ fill[i] = tracks[i]; // put whatever logic you want to populate the values here.
+ }
+ // move from the temp structure to the java structure
+ (*env).SetLongArrayRegion(result, 0, TRACK_SIZE, fill);
+ return result;
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMStereo
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ return fm_radio_set_tuner(fd, 1);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMMono
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ return fm_radio_set_tuner(fd, 0);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMThread
+(__unused JNIEnv *env, __unused jobject thiz, jint fd, jboolean run) {
+ if (run){
+ FMThread = true;
+ fm_radio_thread(fd);
+ }else{
+ FMThread = false;
+ }
+ return FM_SUCCESS;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMBoot
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ fm_radio_set_control(fd, V4L2_CID_S610_IF_COUNT1, 4800); // SetIFCount 1
+ fm_radio_set_control(fd, V4L2_CID_S610_IF_COUNT2, 5600); // SetIFCount 2
+ fm_radio_set_control(fd, V4L2_CID_S610_SOFT_STEREO_BLEND, 3172); // Set Soft Stereo Blend
+ fm_radio_set_control(fd, V4L2_CID_S610_SOFT_MUTE_COEFF, 16); // SetSoftMuteCoeff
+ fm_radio_set_control(fd, V4L2_CID_S610_CH_BAND, S610_BAND_FM); // Set Band (To FM)
+ fm_radio_set_control(fd, V4L2_CID_S610_CH_SPACING, FM_CHANNEL_SPACING_50KHZ); // Spacing 5kHz
+ fm_radio_set_control(fd, V4L2_CID_S610_RDS_ON, FM_RDS_ENABLE); // RDS on
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getNextChannel
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ long ret;
+ fm_radio_channel_searching(fd, 1, 0, FM_CHANNEL_SPACING_100KHZ, &ret);
+ return ret;
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getBeforeChannel
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ long ret;
+ fm_radio_channel_searching(fd, 0, 0, FM_CHANNEL_SPACING_100KHZ, &ret);
+ return ret;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_getAudioChannel
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ int channel = fm_radio_get_tuner(fd);
+ if (channel == V4L2_TUNER_MODE_STEREO){
+ return true; // Is EarPhones
+ }else{
+ return false; // Is Speaker
+ }
+}
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_stopSearching
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ fm_radio_set_control(fd, V4L2_CID_S610_SEEK_CANCEL, 1);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_setFMRSSI
+(__unused JNIEnv *env, __unused jobject thiz, jint fd, jlong rssi) {
+ return fm_radio_set_rssi(fd, rssi);
+}
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_eurekateam_fmradio_NativeFMInterface_closeFMDevice
+(__unused JNIEnv *env, __unused jobject thiz, jint fd) {
+ close(fd);
+}
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/jni/kernel_internal.h b/universal7885-common/apps/FMRadio/jni/kernel_internal.h
new file mode 100644
index 0000000..9f423ce
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/jni/kernel_internal.h
@@ -0,0 +1,125 @@
+
+typedef u_int8_t __u8;
+typedef int32_t __s32;
+typedef u_int32_t __u32;
+
+struct v4l2_tuner {
+ __u32 index;
+ __u8 name[32];
+ __u32 type; /* enum v4l2_tuner_type */
+ __u32 capability;
+ __u32 rangelow;
+ __u32 rangehigh;
+ __u32 rxsubchans;
+ __u32 audmode;
+ __s32 signal;
+ __s32 afc;
+ __u32 reserved[4];
+};
+
+struct v4l2_frequency {
+ __u32 tuner;
+ __u32 type; /* enum v4l2_tuner_type */
+ __u32 frequency;
+ __u32 reserved[8];
+};
+
+
+struct v4l2_hw_freq_seek {
+ __u32 tuner;
+ __u32 type; /* enum v4l2_tuner_type */
+ __u32 seek_upward;
+ __u32 wrap_around;
+ __u32 spacing;
+ __u32 rangelow;
+ __u32 rangehigh;
+ __u32 reserved[5];
+};
+
+enum v4l2_tuner_type {
+ V4L2_TUNER_RADIO = 1,
+ V4L2_TUNER_ANALOG_TV = 2,
+ V4L2_TUNER_DIGITAL_TV = 3,
+ V4L2_TUNER_SDR = 4,
+ V4L2_TUNER_RF = 5,
+};
+struct v4l2_control {
+ __u32 id;
+ __s32 value;
+};
+/**
+ * struct v4l2_capability - Describes V4L2 device caps returned by VIDIOC_QUERYCAP
+ *
+ * @driver: name of the driver module (e.g. "bttv")
+ * @card: name of the card (e.g. "Hauppauge WinTV")
+ * @bus_info: name of the bus (e.g. "PCI:" + pci_name(pci_dev) )
+ * @version: KERNEL_VERSION
+ * @capabilities: capabilities of the physical device as a whole
+ * @device_caps: capabilities accessed via this particular device (node)
+ * @reserved: reserved fields for future extensions
+ */
+struct v4l2_capability {
+ __u8 driver[16];
+ __u8 card[32];
+ __u8 bus_info[32];
+ __u32 version;
+ __u32 capabilities;
+ __u32 device_caps;
+ __u32 reserved[3];
+};
+
+/*
+ * T I M E C O D E
+ */
+struct v4l2_timecode {
+ __u32 type;
+ __u32 flags;
+ __u8 frames;
+ __u8 seconds;
+ __u8 minutes;
+ __u8 hours;
+ __u8 userbits[4];
+};
+
+struct v4l2_buffer {
+ __u32 index;
+ __u32 type;
+ __u32 bytesused;
+ __u32 flags;
+ __u32 field;
+ struct timeval timestamp;
+ struct v4l2_timecode timecode;
+ __u32 sequence;
+
+ /* memory location */
+ __u32 memory;
+ union {
+ __u32 offset;
+ unsigned long userptr;
+ struct v4l2_plane *planes;
+ __s32 fd;
+ } m;
+ __u32 length;
+ __u32 reserved2;
+ __u32 reserved;
+};
+
+#define V4L2_TUNER_MODE_MONO 0x0000
+#define V4L2_TUNER_MODE_STEREO 0x0001
+
+// Ioctl
+#define VIDIOC_G_FREQUENCY _IOWR('V', 56, struct v4l2_frequency)
+#define VIDIOC_S_FREQUENCY _IOW('V', 57, struct v4l2_frequency)
+#define VIDIOC_G_TUNER _IOWR('V', 29, struct v4l2_tuner)
+#define VIDIOC_S_HW_FREQ_SEEK _IOW('V', 82, struct v4l2_hw_freq_seek)
+#define VIDIOC_S_TUNER _IOW('V', 30, struct v4l2_tuner)
+#define VIDIOC_S_CTRL _IOWR('V', 28, struct v4l2_control)
+#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)
+#define VIDIOC_G_CTRL _IOWR('V', 27, struct v4l2_control)
+
+#define V4L2_CTRL_CLASS_USER 0x00980000 /* Old-style 'user' controls */
+#define V4L2_CID_BASE (V4L2_CTRL_CLASS_USER | 0x900)
+#define V4L2_CID_AUDIO_VOLUME (V4L2_CID_BASE+5)
+#define V4L2_CID_AUDIO_MUTE (V4L2_CID_BASE+9)
+
+
diff --git a/universal7885-common/apps/FMRadio/privapp_whitelist_com.eurekateam.fmradio.xml b/universal7885-common/apps/FMRadio/privapp_whitelist_com.eurekateam.fmradio.xml
new file mode 100644
index 0000000..3633515
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/privapp_whitelist_com.eurekateam.fmradio.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/universal7885-common/apps/FMRadio/res/drawable/left.png b/universal7885-common/apps/FMRadio/res/drawable/left.png
new file mode 100644
index 0000000..73bd50a
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/left.png differ
diff --git a/universal7885-common/apps/FMRadio/res/drawable/power.xml b/universal7885-common/apps/FMRadio/res/drawable/power.xml
new file mode 100644
index 0000000..d816c7d
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/res/drawable/power.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/res/drawable/radio.png b/universal7885-common/apps/FMRadio/res/drawable/radio.png
new file mode 100644
index 0000000..3d087e5
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/radio.png differ
diff --git a/universal7885-common/apps/FMRadio/res/drawable/radio_text.png b/universal7885-common/apps/FMRadio/res/drawable/radio_text.png
new file mode 100644
index 0000000..762332f
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/radio_text.png differ
diff --git a/universal7885-common/apps/FMRadio/res/drawable/right.png b/universal7885-common/apps/FMRadio/res/drawable/right.png
new file mode 100644
index 0000000..9108c07
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/right.png differ
diff --git a/universal7885-common/apps/FMRadio/res/drawable/start.png b/universal7885-common/apps/FMRadio/res/drawable/start.png
new file mode 100644
index 0000000..b737620
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/start.png differ
diff --git a/universal7885-common/apps/FMRadio/res/drawable/stop.png b/universal7885-common/apps/FMRadio/res/drawable/stop.png
new file mode 100644
index 0000000..3c7de6a
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/stop.png differ
diff --git a/universal7885-common/apps/FMRadio/res/drawable/warning.jpg b/universal7885-common/apps/FMRadio/res/drawable/warning.jpg
new file mode 100644
index 0000000..bff3977
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/drawable/warning.jpg differ
diff --git a/universal7885-common/apps/FMRadio/res/layout/activity_main.xml b/universal7885-common/apps/FMRadio/res/layout/activity_main.xml
new file mode 100644
index 0000000..fa07f6e
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/res/layout/activity_main.xml
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/res/layout/error_screen.xml b/universal7885-common/apps/FMRadio/res/layout/error_screen.xml
new file mode 100644
index 0000000..a8ad5da
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/res/layout/error_screen.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/res/layout/notification.xml b/universal7885-common/apps/FMRadio/res/layout/notification.xml
new file mode 100644
index 0000000..cce8f6d
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/res/layout/notification.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher.png b/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..80aeee0
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher_foreground.png b/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..64b2197
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher_round.png b/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..ebd5cf6
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher.png b/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..9a96488
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher_foreground.png b/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8c1591b
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher_round.png b/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..cb4ce45
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher.png b/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..17cbab8
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher_foreground.png b/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..515a0ca
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher_round.png b/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..0e4a85c
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher.png b/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..7248392
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher_foreground.png b/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..fa4eba1
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher_round.png b/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..86edfe3
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher.png b/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..e112f21
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..22b5954
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher_round.png b/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8601bfa
Binary files /dev/null and b/universal7885-common/apps/FMRadio/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/universal7885-common/apps/FMRadio/res/values/strings.xml b/universal7885-common/apps/FMRadio/res/values/strings.xml
new file mode 100644
index 0000000..3f90bb5
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/res/values/strings.xml
@@ -0,0 +1,34 @@
+
+ FM Radio
+ FM Radio
+ Output Modes
+ FM Power
+ Disabled
+ Enabled
+ Speaker
+ Wired Earphones
+ Radio Channel
+ Refresh
+ Seek Down
+ Seek Up
+ Scan
+ Stop
+ %s Mhz
+ Quit
+ Album Art
+ Previous Radio Channel
+ Next Radio Channel
+ Play Pause
+ Close Radio
+ Failed to open radio chr device!!
+ The FM radio app needs to call ioctl()
+ to radio chr device (/dev/radio0). If SELinux is enforcing, try making
+ it permissive
+ Failed to check if array != null!!
+ Most likely because FM device couldn\'t
+ find radio channels, if you are sure it should find channels, contact the devs
+ Failed to find wired headset
+ Insert Headphones and try again
+ Radio Controls
+ FM Radio Station
+
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/ErrorHandler.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/ErrorHandler.kt
new file mode 100644
index 0000000..b5b3c34
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/ErrorHandler.kt
@@ -0,0 +1,93 @@
+package com.eurekateam.fmradio
+
+import android.app.Activity
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import android.os.Bundle
+import android.widget.TextView
+import android.widget.Toast
+
+class ErrorHandler: Activity() {
+ private lateinit var mErrorSimple: TextView
+ private lateinit var mErrorDetail: TextView
+ private var mHeadPhonesPresent = false
+ private var mRegistered = false
+ private var fd = 0
+ private lateinit var mContext: Context
+ private lateinit var mAudioManager : AudioManager
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.error_screen)
+ mContext = this
+ fd = intent.getIntExtra("fd", -1)
+ mAudioManager = getSystemService(Service.AUDIO_SERVICE) as AudioManager
+ mErrorDetail = findViewById(R.id.error_detailed)
+ mErrorSimple = findViewById(R.id.error_simple)
+ val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
+ for (i in mAudioDeviceInfo.indices){
+ if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
+ mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES){
+ mHeadPhonesPresent = true
+ }
+ }
+ when (intent?.action){
+
+ RADIO_DEVICE_IO_FAIL -> {
+ mErrorSimple.text = getString(R.string.radio_io_error)
+ mErrorDetail.text = getString(R.string.radio_io_error_desc)
+ }
+ TRACKS_EMPTY -> {
+ mErrorSimple.text = getString(R.string.tracks_empty_error)
+ mErrorDetail.text = getString(R.string.tracks_empty_error_desc)
+ }
+ NO_WIRED_HEADPHONES -> {
+ mErrorSimple.text = getString(R.string.no_headphones_error)
+ mErrorDetail.text = getString(R.string.no_headphones_error_desc)
+ mHeadPhonesPresent = false
+ val receiverFilter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
+ val receiver = mWiredHeadsetReceiver
+ mHeadPhonesPresent = false
+ registerReceiver(receiver, receiverFilter)
+ mRegistered = true
+ }
+ }
+ }
+ private val mWiredHeadsetReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == AudioManager.ACTION_HEADSET_PLUG) {
+ mHeadPhonesPresent = false
+ val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
+ for (i in mAudioDeviceInfo.indices){
+ if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
+ mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES){
+ mHeadPhonesPresent = true
+ }
+ }
+ if (mHeadPhonesPresent){
+ Toast.makeText(mContext, "Wired headphones inserted", Toast.LENGTH_SHORT).show()
+ startActivity(Intent(mContext, MainActivity::class.java))
+ }
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ if (mRegistered)
+ unregisterReceiver(mWiredHeadsetReceiver)
+ }
+
+ companion object {
+ private const val PACKAGENAME = "com.eurekateam.fmradio"
+ // Failures
+ private const val RADIO_DEVICE_IO_FAIL = "${PACKAGENAME}.RADIO.IO"
+ private const val TRACKS_EMPTY = "${PACKAGENAME}.TRACKS"
+ private const val NO_WIRED_HEADPHONES = "${PACKAGENAME}.NO_HEADPHONE"
+
+ }
+}
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt
new file mode 100644
index 0000000..2597668
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt
@@ -0,0 +1,207 @@
+package com.eurekateam.fmradio
+
+import android.app.*
+import android.content.Context
+import android.content.Intent
+import android.media.AudioManager
+import android.os.IBinder
+import android.util.Log
+import android.widget.RemoteViews
+import androidx.core.app.NotificationCompat
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStream
+
+
+class FMRadioService : Service() {
+ private lateinit var mContext: Context
+ private lateinit var mAudioManager: AudioManager
+ private var mIsPlaying = true
+ private lateinit var mTracks : LongArray
+ private var mTitle : String = ""
+ private var fd : Int = -1
+ private var mIndex = -1
+ private lateinit var mNativeFMInterface: NativeFMInterface
+ private lateinit var pendingIntent: PendingIntent
+ private lateinit var contentView : RemoteViews
+ private lateinit var mFilePath : File
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent != null) {
+ var mShutDown = false
+ Log.i(TAG, "onStartCommand: Received ${intent.action}")
+ when (intent.action) {
+ BEGIN_BG_SERVICE -> {
+ mContext = this
+ mFilePath = mContext.filesDir
+ contentView = RemoteViews(mContext.packageName, R.layout.notification)
+ mNativeFMInterface = NativeFMInterface()
+ fd = intent.getIntExtra("fd", -1)
+ mTracks = intent.getLongArrayExtra("tracks")!!
+ mIndex = intent.getIntExtra("CurrentIndex", -1)
+ mTitle = "FM ${mTracks[mIndex].toFloat() / 1000} Mhz"
+ contentView.setImageViewResource(
+ R.id.btn_play_pause,
+ R.drawable.stop
+ )
+ mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
+ setupRemoteView()
+ // Create an explicit intent for an Activity in your app
+ val onClickIntent = Intent(this, FMRadioService::class.java).apply {
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ onClickIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ mIsPlaying = true
+ startForeground(51, pushNotification())
+ }
+ ACTION_PLAY -> {
+ mIsPlaying = !mIsPlaying
+ mTitle =
+ if (mIsPlaying) {
+ "FM ${mTracks[mIndex].toFloat() / 1000}Mhz"
+ } else {
+ "FM ${mTracks[mIndex].toFloat() / 1000}Mhz - Stopped"
+ }
+ if (mIsPlaying) {
+ contentView.setImageViewResource(
+ R.id.btn_play_pause,
+ R.drawable.stop
+ )
+ writeToFile("fm_radio_state", "1")
+ mAudioManager.setParameters(FM_RADIO_ON)
+
+ } else {
+ mAudioManager.setParameters(FM_RADIO_OFF)
+ contentView.setImageViewResource(
+ R.id.btn_play_pause,
+ R.drawable.start
+ )
+ writeToFile("fm_radio_state", "0")
+ }
+ }
+ ACTION_BEFORE -> {
+ mIsPlaying = true
+ contentView.setImageViewResource(
+ R.id.btn_play_pause,
+ R.drawable.stop
+ )
+ Log.i(TAG, "onStartCommand: mCurrentIndex $mIndex")
+ if (mIndex > 0)
+ mIndex -= 1
+ if (mIndex >= 1)
+ mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt())
+ mTitle = if (mIsPlaying) {
+ "FM ${mTracks[mIndex].toFloat() / 1000}Mhz"
+ } else {
+ "FM ${mTracks[mIndex].toFloat() / 1000}Mhz - Stopped"
+ }
+ }
+ ACTION_NEXT -> {
+ mIsPlaying = true
+ contentView.setImageViewResource(
+ R.id.btn_play_pause,
+ R.drawable.stop
+ )
+ Log.i(TAG, "onStartCommand: mCurrentIndex $mIndex")
+ if (mIndex < mTracks.size - 1)
+ mIndex += 1
+ if (mIndex < mTracks.size - 1)
+ mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt())
+ mTitle = if (mIsPlaying) {
+ "FM ${mTracks[mIndex].toFloat() / 1000}Mhz"
+ } else {
+ "FM ${mTracks[mIndex].toFloat() / 1000}Mhz - Stopped"
+ }
+ }
+ ACTION_QUIT -> {
+ mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
+ mAudioManager.setParameters(FM_RADIO_OFF)
+ stopForeground(true)
+ mShutDown = true
+ }
+ STOP_BG_SERVICE -> {
+ val mFreqToWrite = mTracks[mIndex].toString()
+ writeToFile(
+ "fm_radio_freq_current",
+ mFreqToWrite
+ )
+ }
+ else -> {
+ throw IllegalAccessException("Unexpected Intent!")
+ }
+ }
+ Log.i(TAG, "onStartCommand: mTitle=$mTitle")
+ if (!mShutDown) {
+ contentView.setTextViewText(R.id.txt_title, mTitle)
+ startForeground(51,pushNotification())
+ }
+ }
+ return START_STICKY
+ }
+ private fun writeToFile(fileName : String, data: String){
+ var os: OutputStream? = null
+ val mData = data.padStart(6,'0')
+ try {
+ os = FileOutputStream(File(mFilePath.absolutePath + "/" + fileName))
+ os.write(mData.toByteArray(), 0, mData.length)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } finally {
+ try {
+ os?.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+ private fun setupRemoteView() {
+ val actionTogglePlay = Intent(ACTION_PLAY)
+ val actionForward = Intent(ACTION_NEXT)
+ val actionRewind = Intent(ACTION_BEFORE)
+ val actionClose = Intent(ACTION_QUIT)
+ val togglePlay = PendingIntent.getService(mContext, 0, actionTogglePlay, PendingIntent.FLAG_IMMUTABLE)
+ val forward = PendingIntent.getService(mContext, 0, actionForward, PendingIntent.FLAG_IMMUTABLE)
+ val rewind = PendingIntent.getService(mContext, 0, actionRewind, PendingIntent.FLAG_IMMUTABLE)
+ val close = PendingIntent.getService(mContext, 0, actionClose, PendingIntent.FLAG_IMMUTABLE)
+ contentView.setOnClickPendingIntent(R.id.btn_play_pause, togglePlay)
+ contentView.setOnClickPendingIntent(R.id.btn_forward, forward)
+ contentView.setOnClickPendingIntent(R.id.btn_rewind, rewind)
+ contentView.setOnClickPendingIntent(R.id.btn_close, close)
+ }
+
+ private fun pushNotification(): Notification {
+ val nm = mContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ val channel = NotificationChannel(
+ mContext.packageName, "FM Radio Playing",
+ NotificationManager.IMPORTANCE_NONE
+ )
+ nm.createNotificationChannel(channel)
+ val builder = NotificationCompat.Builder(mContext, mContext.packageName)
+ builder.setSmallIcon(R.drawable.radio_text)
+ builder.setOngoing(true)
+ builder.setCustomBigContentView(contentView)
+ builder.setContentIntent(pendingIntent)
+ builder.setChannelId(mContext.packageName)
+ return builder.build()
+ }
+ companion object {
+ private const val PACKAGENAME = "com.eurekateam.fmradio"
+ private const val BEGIN_BG_SERVICE = "$PACKAGENAME.STARTBG"
+ private const val ACTION_PLAY = "$PACKAGENAME.PLAY_FM"
+ private const val ACTION_NEXT = "$PACKAGENAME.NEXT"
+ private const val ACTION_BEFORE = "$PACKAGENAME.BEFORE"
+ private const val ACTION_QUIT = "$PACKAGENAME.QUIT"
+ private const val FM_RADIO_ON = "l_fmradio_mode=on"
+ private const val FM_RADIO_OFF = "l_fmradio_mode=off"
+ private const val TAG = "FMRadioService"
+ private const val STOP_BG_SERVICE = "${PACKAGENAME}.STOPBG"
+ }
+}
\ No newline at end of file
diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt
new file mode 100644
index 0000000..b766b41
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt
@@ -0,0 +1,610 @@
+package com.eurekateam.fmradio
+
+import android.bluetooth.BluetoothAdapter
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioAttributes
+import android.media.AudioDeviceInfo
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.util.Log
+import android.view.KeyEvent
+import android.view.View
+import android.widget.Button
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import de.dlyt.yanndroid.oneui.view.SeekBar
+import java.io.*
+import java.text.DecimalFormat
+
+class MainActivity : AppCompatActivity(),
+ View.OnClickListener, SeekBar.OnSeekBarChangeListener, AudioManager.OnAudioFocusChangeListener {
+ private lateinit var mAudioManager: AudioManager
+ private lateinit var mStreamSwitch : Button
+ private lateinit var mFMSwitch : Button
+ private lateinit var mSeekUpBtn: Button
+ private lateinit var mSeekDownBtn : Button
+ private lateinit var mBandSelect : TextView
+ private var mFMThread: Thread? = null
+ private var fd = -1
+ private var mCurrentFreq = 0
+ private lateinit var mNativeFMInterface: NativeFMInterface
+ private lateinit var tracks : LongArray
+ private lateinit var mContext : Context
+ private lateinit var mTitle : TextView
+ private lateinit var mScanBtn : Button
+ private lateinit var mScanStopBtn : Button
+ private lateinit var mSeekBar: SeekBar
+ private var mCurrentIndex = 0
+ private val format: DecimalFormat = DecimalFormat("0.#")
+ private var mVolume : Int = 0
+ private lateinit var scanThread: Thread
+ private var mScanThread: Thread? = null
+ private lateinit var mIntent: Intent
+ private var mServiceRegistered = false
+ private lateinit var mFilePath : File
+ private var mLastBackPress: Long = 0L
+ private var mScanning = false
+ private lateinit var mErrorIntent : Intent
+ private var mFreqInitialized = false
+ private lateinit var mDebugStringThread: Thread
+ private lateinit var mDebugInfo : TextView
+ private var mDebugThread : Thread? = null
+ private lateinit var mQuitButton: Button
+ private var mQuit = false
+ private var mHeadSetPlugged : Boolean = false
+ private val mMainHandler : Handler = Handler(Looper.getMainLooper())
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ System.loadLibrary("fmradioioctl_jni")
+ setContentView(R.layout.activity_main)
+ mContext = this
+ mFilePath = mContext.filesDir
+ mNativeFMInterface = NativeFMInterface()
+ mIntent = Intent(mContext, FMRadioService::class.java)
+ mErrorIntent = Intent(mContext, ErrorHandler::class.java)
+ mErrorIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ mCurrentFreq = mNativeFMInterface.getFMLower(fd)
+ mTitle = findViewById(R.id.fm_radio_title)
+ mStreamSwitch = findViewById(R.id.fm_output)
+ mFMSwitch = findViewById(R.id.fmswitch)
+ mSeekBar = findViewById(R.id.fm_freq_seekbar)
+ mSeekDownBtn = findViewById(R.id.seek_down)
+ mSeekUpBtn = findViewById(R.id.seek_up)
+ mBandSelect = findViewById(R.id.band_select)
+ mScanBtn = findViewById(R.id.scan_through)
+ mScanStopBtn = findViewById(R.id.stop_scan)
+ mDebugInfo = findViewById(R.id.debug_info)
+ mQuitButton = findViewById(R.id.quit_fm)
+ mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
+ mStreamSwitch.setOnClickListener(this)
+ mFMSwitch.setOnClickListener(this)
+ mSeekDownBtn.setOnClickListener(this)
+ mSeekUpBtn.setOnClickListener(this)
+ mQuitButton.setOnClickListener(this)
+ mScanBtn.setOnClickListener(this)
+ mScanStopBtn.isEnabled = false
+ mStreamSwitch.text = if(!mAudioManager.isSpeakerphoneOn)
+ getString(R.string.wired_earphones) else getString(R.string.phone_speaker)
+ val receiverFilter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
+ val mBtFilter = IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
+ registerReceiver(mBtHeadsetReceiver, mBtFilter)
+ registerReceiver(mWiredHeadsetReceiver, receiverFilter)
+ val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
+ for (i in mAudioDeviceInfo.indices){
+ if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
+ mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
+ mHeadSetPlugged = true
+ Log.i(TAG, "onCreate: Wired Headphones detected")
+ }
+ }
+ fd = mNativeFMInterface.openFMDevice()
+ if (fd == -1){
+ Log.e(TAG, "onCreate: CANNOT OPEN /dev/radio0!!!")
+ mErrorIntent.action = RADIO_DEVICE_IO_FAIL
+ startActivity(mErrorIntent)
+ finish()
+ }
+ mErrorIntent.putExtra("fd", fd)
+ if (!mHeadSetPlugged){
+ Toast.makeText(mContext, "Wired Headphones not detected", Toast.LENGTH_SHORT).show()
+ Thread{
+ Thread.sleep(1500)
+ mainExecutor.execute {
+ mErrorIntent.action = NO_WIRED_HEADPHONES
+ mNativeFMInterface.setFMMute(fd, true)
+ startActivity(mErrorIntent)
+ finish()
+ }
+ }.start()
+ }
+ mScanStopBtn.setOnClickListener(this)
+ if (File(mFilePath.absolutePath + "/fm_radio_volume").exists()) {
+ mVolume = if (readFromFile("fm_radio_volume").isEmpty()){
+ 10
+ }else{
+ readFromFile("fm_radio_volume").toInt()
+ }
+ }
+ Thread{
+ tracks = mNativeFMInterface.getFMTracks(fd)
+ tracks = removeZeros(tracks)
+ val mPreFreq = if (File(mFilePath.absolutePath + "/fm_radio_freq_current").exists()){
+ readFromFile("fm_radio_freq_current")
+ }else{
+ tracks[0].toString()
+ }
+ if (DEBUG) Log.i(TAG, "onCreate: file value = $mPreFreq")
+ for (i in tracks.indices){
+ if (tracks[i] == mPreFreq.toLong()){
+ mNativeFMInterface.setFMFreq(fd,mPreFreq.toInt())
+ mBandSelect.text = String.format(getString(R.string.mhz), mPreFreq.toFloat() / 1000)
+ mCurrentFreq = mPreFreq.toInt()
+ mCurrentIndex = i
+ break
+ }
+ }
+ if (File(mFilePath.absolutePath + "/fm_radio_state").exists()){
+ if(readFromFile("fm_radio_state") == "1"){
+ mainExecutor.execute {
+ mFMSwitch.text = getString(R.string.enabled)
+ }
+ mAudioManager.setParameters(FM_RADIO_ON)
+ mNativeFMInterface.setFMBoot(fd)
+ mNativeFMInterface.setFMFreq(fd, mCurrentFreq)
+ mFMThread = Thread {
+ mNativeFMInterface.setFMThread(fd,true)
+ }
+ mFMThread!!.start()
+ mDebugThread = Thread(mDebugStringThread)
+ mDebugThread!!.start()
+ }else{
+ mainExecutor.execute {
+ mFMSwitch.text = getString(R.string.disabled)
+ }
+ mFMThread = null
+ mDebugThread = null
+ mAudioManager.setParameters(FM_RADIO_OFF)
+ mNativeFMInterface.setFMThread(fd,false)
+ }
+ }
+ mSeekBar.max = tracks.size - 1
+ mSeekBar.min = 0
+ mSeekBar.progress = mCurrentIndex
+ mSeekBar.setOnSeekBarChangeListener(this)
+ mAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
+ if (DEBUG)
+ Log.d(TAG, "onCreate: reading $mFilePath/headphones")
+ if (File("$mFilePath/headphones").exists()){
+ mAudioManager.isSpeakerphoneOn = false
+ mStreamSwitch.text = getString(R.string.wired_earphones)
+ }else{
+ mAudioManager.isSpeakerphoneOn = true
+ mStreamSwitch.text = getString(R.string.phone_speaker)
+ }
+ mFreqInitialized = true
+ }.start()
+ mFMThread = Thread {
+ mNativeFMInterface.setFMThread(fd,true)
+ }
+ scanThread = Thread {
+ mFreqInitialized = false
+ val mOriginalIndex = mCurrentIndex
+ mainExecutor.execute {
+ mScanStopBtn.isEnabled = true
+ mSeekBar.isEnabled = false
+ }
+ var count = 0
+ mNativeFMInterface.setFMMute(fd, false)
+ while (count < tracks.size && mScanning) {
+ mNativeFMInterface.setFMFreq(fd, tracks[count].toInt())
+ mainExecutor.execute {
+ mBandSelect.text = String.format(getString(R.string.mhz), format.format(tracks[count].toFloat() / 1000))
+ mSeekBar.progress = mCurrentIndex
+ }
+ mCurrentFreq = tracks[count].toInt()
+ mCurrentIndex = count
+ Thread.sleep(1500)
+ count++
+ }
+ mainExecutor.execute {
+ mScanBtn.isEnabled = true
+ mScanStopBtn.isEnabled = false
+ mSeekBar.isEnabled = true
+ }
+ if (mScanning)
+ mCurrentIndex = mOriginalIndex
+
+ mNativeFMInterface.setFMFreq(fd, tracks[mCurrentIndex].toInt())
+ mBandSelect.text = String.format(getString(R.string.mhz), format.format(tracks[mCurrentIndex].toFloat() / 1000))
+ mCurrentFreq = tracks[mCurrentIndex].toInt()
+ mSeekBar.progress = mCurrentIndex
+ mScanning = false
+ mFreqInitialized = true
+ }
+ mAudioManager.requestAudioFocus(mFocusRequest)
+ mNativeFMInterface.setFMVolume(fd, mVolume)
+ mDebugStringThread = Thread{
+ while (true){
+ mainExecutor.execute {
+ mDebugInfo.text = String.format(DEBUG_STRING, fd, mNativeFMInterface.getRMSSI(fd))
+ }
+ Thread.sleep(1000)
+ }
+ }
+
+ }
+ private val mWiredHeadsetReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == AudioManager.ACTION_HEADSET_PLUG) {
+ mHeadSetPlugged = false
+ val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
+ for (i in mAudioDeviceInfo.indices){
+ if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
+ mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES){
+ mHeadSetPlugged = true
+ }
+ }
+ if (!mHeadSetPlugged){
+ Log.w(TAG, "onReceive: Headset Unplugged")
+ Toast.makeText(mContext, "HeadSet disconnected", Toast.LENGTH_SHORT).show()
+ mErrorIntent.action = NO_WIRED_HEADPHONES
+ unregisterReceiver(this)
+ mNativeFMInterface.setFMMute(fd, true)
+ startActivity(mErrorIntent)
+ finish()
+ }
+ }
+ }
+ }
+ private val mBtHeadsetReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) {
+ when (intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.ERROR)){
+ BluetoothAdapter.STATE_CONNECTED -> {
+ Log.i(TAG, "onReceive: BT Connected, SCO ${mAudioManager.isBluetoothScoOn}")
+ Toast.makeText(mContext, "Warning: FM via BT not supported", Toast.LENGTH_SHORT).show()
+ }
+ BluetoothAdapter.STATE_DISCONNECTED -> {
+ Log.i(TAG, "onReceive: BT Disconnected SCO ${mAudioManager.isBluetoothScoOn}")
+ }
+ }
+ }
+ }
+ }
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ val action = event.action
+ val mHandler = Handler(Looper.getMainLooper())
+ return when (event.keyCode) {
+ KeyEvent.KEYCODE_VOLUME_UP -> {
+ if (action == KeyEvent.ACTION_UP) {
+ if (mVolume < 15)
+ mVolume += 1
+ mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI)
+ mNativeFMInterface.setFMVolume(fd, mVolume)
+ mTitle.text = String.format("Volume %d", mVolume)
+ mHandler.postDelayed({
+ mTitle.text = getString(R.string.fm_radio)
+ },1500)
+ }
+ true
+ }
+ KeyEvent.KEYCODE_VOLUME_DOWN -> {
+ if (action == KeyEvent.ACTION_UP) {
+ if (mVolume > 0)
+ mVolume -= 1
+ mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI)
+ mNativeFMInterface.setFMVolume(fd, mVolume)
+ mTitle.text = String.format("Volume %d", mVolume)
+ mHandler.postDelayed({
+ mTitle.text = getString(R.string.fm_radio)
+ },1500)
+ }
+ true
+ }
+ else -> super.dispatchKeyEvent(event)
+ }
+ }
+ override fun onClick(v: View) {
+ if (v.id == R.id.fmswitch){
+ if(mDebugThread == null){
+ mAudioManager.setParameters(FM_RADIO_ON)
+ mNativeFMInterface.setFMBoot(fd)
+ mNativeFMInterface.setFMFreq(fd, mCurrentFreq)
+ mFMSwitch.text = getString(R.string.enabled)
+ writeToFile("fm_radio_state", "1")
+ mFMThread = Thread {
+ mNativeFMInterface.setFMThread(fd,true)
+ }
+ mFMThread!!.start()
+ mDebugThread = Thread(mDebugStringThread)
+ mDebugThread!!.start()
+ }else{
+ mFMThread = null
+ mDebugThread = null
+ mAudioManager.setParameters(FM_RADIO_OFF)
+ mNativeFMInterface.setFMThread(fd,false)
+ writeToFile("fm_radio_state", "0")
+ mFMSwitch.text = getString(R.string.disabled)
+ }
+ }else if (v.id == R.id.fm_output) {
+ /***
+ * Some Notes about ABOX
+ * service call media.audio_policy 5 i32 i32 2
+ * when mode 1 -> normal
+ * mode 2 -> telephony mode
+ * mode 3 -> terminate telephony mode
+ * mode 4 -> nothing
+ * from 5 -> doesn't exist
+ */
+ mAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
+ if (mStreamSwitch.text == getString(R.string.wired_earphones)) {
+ Log.i(TAG, "New Output: Speaker")
+ mAudioManager.isSpeakerphoneOn = true
+ File("$mFilePath/headphones").delete()
+ mStreamSwitch.text = getString(R.string.phone_speaker)
+ } else {
+ Log.i(TAG, "New Output: Headset")
+ mAudioManager.isSpeakerphoneOn = false
+ File("$mFilePath/headphones").createNewFile()
+ mStreamSwitch.text = getString(R.string.wired_earphones)
+ }
+ mAudioManager.setParameters(FM_RADIO_OFF)
+ mAudioManager.setParameters(FM_RADIO_ON)
+ mAudioManager.mode = AudioManager.MODE_NORMAL
+ }else {
+ var mExit = false
+ if (!mFreqInitialized && v.id != R.id.stop_scan) {
+ Toast.makeText(mContext, "FM Radio is still scanning...", Toast.LENGTH_SHORT).show()
+ } else {
+ if (tracks.isEmpty()) {
+ Log.e(TAG, "onClick: tracks is empty!!!")
+ mErrorIntent.action = TRACKS_EMPTY
+ startActivity(mErrorIntent)
+ }
+ mNativeFMInterface.setFMMute(fd, true)
+ when (v.id) {
+ R.id.seek_up -> mCurrentIndex += 1
+ R.id.seek_down -> mCurrentIndex -= 1
+ R.id.scan_through -> {
+ mScanning = true
+ mScanBtn.isEnabled = false
+ mScanThread = null
+ mScanThread = Thread(scanThread)
+ mScanThread!!.start()
+ mExit = true
+ }
+ R.id.stop_scan -> {
+ mScanning = false
+ mNativeFMInterface.setFMMute(fd, false)
+ mExit = true
+ }
+ R.id.quit_fm -> {
+ mQuit = true
+ finish()
+ }
+ else -> throw IllegalArgumentException("Unexpected Onclick Event!!")
+ }
+ if (!mExit)
+ updateFreq()
+ }
+ }
+ }
+
+
+ private fun updateFreq(){
+ if (mCurrentIndex < 0 || mCurrentIndex >= tracks.size) mCurrentIndex = 0
+ mCurrentFreq = tracks[mCurrentIndex].toInt()
+ writeToFile("fm_radio_freq_current", mCurrentFreq.toString())
+ mNativeFMInterface.setFMFreq(fd, mCurrentFreq)
+ mainExecutor.execute {
+ mBandSelect.text = String.format("%s Mhz", format.format(mCurrentFreq.toFloat() / 1000))
+ mSeekBar.progress = mCurrentIndex
+ }
+ mNativeFMInterface.setFMMute(fd, false)
+ }
+ companion object {
+ // Abox flags
+ private const val FM_RADIO_ON = "l_fmradio_mode=on"
+ private const val FM_RADIO_OFF = "l_fmradio_mode=off"
+ private const val TAG = "FMRadio"
+
+ private const val DEBUG = true
+
+ // Backgroud service intents
+ private const val PACKAGENAME = "com.eurekateam.fmradio"
+ private const val BEGIN_BG_SERVICE = "$PACKAGENAME.STARTBG"
+ private const val STOP_BG_SERVICE = "$PACKAGENAME.STOPBG"
+ private const val QUIT = "$PACKAGENAME.QUIT"
+
+ // Failures
+ private const val RADIO_DEVICE_IO_FAIL = "$PACKAGENAME.RADIO.IO"
+ private const val TRACKS_EMPTY = "$PACKAGENAME.TRACKS"
+ private const val NO_WIRED_HEADPHONES = "$PACKAGENAME.NO_HEADPHONE"
+ private const val DEBUG_STRING = "fd = %d, rssi = %d"
+ }
+
+ private fun removeZeros(array : LongArray): LongArray{
+ var count = 0
+ if (DEBUG) Log.d(TAG, "removeZeros: Got array")
+ for (i in array.indices){
+ if(array[i] != 0L){
+ count++
+ }
+ }
+ if (DEBUG) Log.d(TAG, "removeZeros: Removed ${array.size - count} of zeros")
+ return array.sliceArray(0 until count)
+ }
+
+ override fun onBackPressed() {
+ if (SystemClock.uptimeMillis() - mLastBackPress in 0..1500){
+ super.onBackPressed()
+ }else{
+ Toast.makeText(mContext, "Press back again to quit", Toast.LENGTH_SHORT).show()
+ }
+ mLastBackPress = SystemClock.uptimeMillis()
+ }
+ override fun onPause() {
+ super.onPause()
+ if (mQuit){
+ mNativeFMInterface.closeFMDevice(fd)
+ mIntent.action = QUIT
+ startService(mIntent)
+ mAudioManager.abandonAudioFocusRequest(mFocusRequest)
+ writeToFile("fm_radio_state","0")
+ }else {
+ if (readFromFile("fm_radio_state") == "1" && mFreqInitialized) {
+ Log.i(TAG, "onPause: Starting background service")
+ for (i in tracks.indices){
+ if (tracks[i] == mCurrentFreq.toLong()){
+ mCurrentIndex = i
+ }
+ }
+ Log.d(TAG, "onPause: params: fd=$fd, " +
+ "tracks.size=${tracks.size}, mCurrentIndex: $mCurrentIndex")
+ mIntent.action = BEGIN_BG_SERVICE
+ mIntent.putExtra("fd", fd)
+ mIntent.putExtra("tracks", tracks)
+ mIntent.putExtra("freq", mCurrentFreq )
+ mIntent.putExtra("CurrentIndex", mCurrentIndex )
+ startService(mIntent)
+ mServiceRegistered = true
+ }
+ }
+ writeToFile("fm_radio_volume", mVolume.toString())
+ }
+
+ private val mFocusRequest: AudioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
+ setAudioAttributes(AudioAttributes.Builder().run {
+ setUsage(AudioAttributes.USAGE_MEDIA)
+ setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ build()
+ })
+ setAcceptsDelayedFocusGain(true)
+ setOnAudioFocusChangeListener(this@MainActivity, mMainHandler)
+ build()
+ }
+ // implementing OnAudioFocusChangeListener to react to focus changes
+ override fun onAudioFocusChange(focusChange: Int) {
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_GAIN -> {
+ if (DEBUG)
+ Log.i(TAG, "onAudioFocusChange: AudioFocus Gain")
+ mNativeFMInterface.setFMVolume(fd, mVolume)
+ mAudioManager.setParameters(FM_RADIO_ON)
+ mNativeFMInterface.setFMFreq(fd, mCurrentFreq)
+ mFMSwitch.text = getString(R.string.enabled)
+ writeToFile("fm_radio_state", "1")
+ mFMThread = Thread {
+ mNativeFMInterface.setFMThread(fd,true)
+ }
+ mFMThread!!.start()
+ mDebugThread = Thread(mDebugStringThread)
+ mDebugThread!!.start()
+ }
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ if (DEBUG)
+ Log.i(TAG, "onAudioFocusChange: AudioFocus Loss")
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
+ if (DEBUG)
+ Log.i(TAG, "onAudioFocusChange: AudioFocus Loss (Can Duck)")
+ mNativeFMInterface.setFMVolume(fd, 2)
+ }
+ }
+ }
+ override fun onRestart() {
+ super.onRestart()
+ if (mServiceRegistered) {
+ mAudioManager.requestAudioFocus(mFocusRequest)
+ Log.i(TAG, "onRestart: Stopping service")
+ mIntent.action = STOP_BG_SERVICE
+ startService(mIntent)
+ mCurrentFreq = readFromFile("fm_radio_freq_current").toInt()
+ if(readFromFile("fm_radio_state") == "1"){
+ mFMThread = Thread {
+ mNativeFMInterface.setFMThread(fd,true)
+ }
+ mFMThread!!.start()
+ mDebugThread = null
+ mDebugThread = Thread(mDebugStringThread)
+ mDebugThread!!.start()
+ mFMSwitch.text = getString(R.string.enabled)
+ }else{
+ mFMThread = null
+ mDebugThread = null
+ mNativeFMInterface.setFMThread(fd,false)
+ mFMSwitch.text = getString(R.string.disabled)
+ }
+ mServiceRegistered = false
+ }
+ }
+
+ private fun writeToFile(fileName : String, data: String){
+ var os: OutputStream? = null
+ val mData = data.padStart(6,'0')
+ try {
+ os = FileOutputStream(File(mFilePath.absolutePath + "/" + fileName))
+ if (DEBUG) Log.i(TAG, "writeToFile: Writing $mData to $fileName")
+ os.write(mData.toByteArray(), 0, mData.length)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } finally {
+ try {
+ os?.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+ private fun readFromFile(fileName: String): String{
+ var os: InputStream? = null
+ val mByteArray = ByteArray(6)
+ try {
+ os = FileInputStream(File(mFilePath.absolutePath + "/" + fileName))
+ if (DEBUG) Log.i(TAG, "readFromFile: Reading from $fileName")
+ os.read(mByteArray)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } finally {
+ try {
+ os?.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ return String(mByteArray).trimStart('0')
+ }
+
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ if (DEBUG)
+ Log.i(TAG, "onProgressChanged: Value changed to $progress, matching FM freq is ${tracks[progress]}")
+ mBandSelect.text = String.format(getString(R.string.mhz),
+ format.format(tracks[seekBar.progress].toFloat()/1000))
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {
+ mNativeFMInterface.setFMMute(fd, true)
+ if (DEBUG)
+ Log.i(TAG, "onStartTrackingTouch: user touched.")
+
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {
+ mCurrentIndex = seekBar.progress
+ mCurrentFreq = tracks[seekBar.progress].toInt()
+ Log.d(TAG, "onStopTrackingTouch: user stopped touching")
+ writeToFile("fm_radio_freq_current", mCurrentFreq.toString())
+ mNativeFMInterface.setFMFreq(fd, mCurrentFreq)
+ mBandSelect.text = String.format("%s Mhz", format.format(mCurrentFreq.toFloat() / 1000))
+ mNativeFMInterface.setFMMute(fd, false)
+ }
+
+}
diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt
new file mode 100644
index 0000000..78bccce
--- /dev/null
+++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt
@@ -0,0 +1,23 @@
+package com.eurekateam.fmradio
+
+class NativeFMInterface {
+ external fun openFMDevice() : Int
+ external fun getFMFreq(fd : Int): Long
+ external fun setFMFreq(fd : Int, freq : Int): Int
+ external fun setFMVolume(fd: Int, volume: Int /* 1 - 15 */): Int
+ external fun setFMMute(fd: Int, mute: Boolean): Int
+ external fun getFmUpper(fd: Int): Int
+ external fun getFMLower(fd: Int): Int
+ external fun getRMSSI(fd: Int): Int
+ external fun getFMTracks(fd: Int) : LongArray
+ external fun setFMStereo(fd: Int): Int
+ external fun setFMMono(fd: Int): Int
+ external fun setFMThread(fd: Int, run: Boolean): Int
+ external fun setFMBoot(fd: Int)
+ external fun getNextChannel(fd: Int): Int
+ external fun getBeforeChannel(fd: Int): Int
+ external fun getAudioChannel(fd: Int): Boolean // Stereo == true, Mono == false
+ external fun stopSearching(fd: Int)
+ external fun setFMRSSI(fd: Int, rssi: Long): Int
+ external fun closeFMDevice(fd: Int)
+}
\ No newline at end of file