universal7885: Add FMRadio app
* Created from scratch by me (Royna) with help of LineageOS repo [1], Sony Xperia Dev repo [2] * Basic input/output, channel selection is working [1]: https://github.com/LineageOS/android_hardware_samsung_slsi_fm [2]: https://github.com/sonyxperiadev/vendor-qcom-opensource-fm Change-Id: I922bbeb9f86e6a4c7bc993d27d2f9eeb2a712186 Signed-off-by: roynatech2544 <whiteshell2544@naver.com>
21
universal7885-common/apps/FMRadio/Android.bp
Normal file
|
|
@ -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",
|
||||||
|
}
|
||||||
48
universal7885-common/apps/FMRadio/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.eurekateam.fmradio"
|
||||||
|
android:sharedUserId="android.uid.system">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/OneUI4Theme">
|
||||||
|
<activity
|
||||||
|
android:name=".ErrorHandler"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.HEADSET_PLUG"/>
|
||||||
|
<action android:name="com.eurekateam.fmradio.RADIO.IO" />
|
||||||
|
<action android:name="com.eurekateam.fmradio.TRACKS" />
|
||||||
|
<action android:name="com.eurekateam.fmradio.NO_HEADPHONE" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<action android:name="android.intent.action.HEADSET_PLUG"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".FMRadioService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.eurekateam.fmradio.STARTBG" />
|
||||||
|
<action android:name="com.eurekateam.fmradio.PLAY_FM"/>
|
||||||
|
<action android:name="com.eurekateam.fmradio.NEXT"/>
|
||||||
|
<action android:name="com.eurekateam.fmradio.BEFORE"/>
|
||||||
|
<action android:name="com.eurekateam.fmradio.QUIT"/>
|
||||||
|
<action android:name="com.eurekateam.fmradio.STOPBG"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
12
universal7885-common/apps/FMRadio/jni/Android.bp
Normal file
|
|
@ -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"],
|
||||||
|
}
|
||||||
|
|
||||||
65
universal7885-common/apps/FMRadio/jni/fm.h
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
484
universal7885-common/apps/FMRadio/jni/fm_ioctl.cpp
Normal file
|
|
@ -0,0 +1,484 @@
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/poll.h>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <iostream>
|
||||||
|
#include "fm.h"
|
||||||
|
#include "kernel_internal.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
//#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<class C, typename T>
|
||||||
|
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);
|
||||||
|
}
|
||||||
125
universal7885-common/apps/FMRadio/jni/kernel_internal.h
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2021 Eureka Team
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<permissions>
|
||||||
|
<privapp-permissions package="com.eurekateam.fmradio">
|
||||||
|
<permission name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
|
</privapp-permissions>
|
||||||
|
</permissions>
|
||||||
BIN
universal7885-common/apps/FMRadio/res/drawable/left.png
Normal file
|
After Width: | Height: | Size: 452 B |
12
universal7885-common/apps/FMRadio/res/drawable/power.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24.0dp"
|
||||||
|
android:height="24.0dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M11,2h2v10h-2z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M18.37,5.64l-1.41,1.41c2.73,2.73 2.72,7.16 -0.01,9.89c-2.73,2.73 -7.17,2.73 -9.89,0.01c-2.73,-2.73 -2.74,-7.18 -0.01,-9.91L5.64,5.64c-3.51,3.51 -3.51,9.21 0.01,12.73c3.51,3.51 9.21,3.51 12.72,-0.01C21.88,14.85 21.88,9.16 18.37,5.64z"/>
|
||||||
|
</vector>
|
||||||
BIN
universal7885-common/apps/FMRadio/res/drawable/radio.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
universal7885-common/apps/FMRadio/res/drawable/radio_text.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
universal7885-common/apps/FMRadio/res/drawable/right.png
Normal file
|
After Width: | Height: | Size: 434 B |
BIN
universal7885-common/apps/FMRadio/res/drawable/start.png
Normal file
|
After Width: | Height: | Size: 457 B |
BIN
universal7885-common/apps/FMRadio/res/drawable/stop.png
Normal file
|
After Width: | Height: | Size: 305 B |
BIN
universal7885-common/apps/FMRadio/res/drawable/warning.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
310
universal7885-common/apps/FMRadio/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="top|center"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/debug_info"
|
||||||
|
android:layout_width="190dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp" />
|
||||||
|
|
||||||
|
<de.dlyt.yanndroid.oneui.layout.RoundLinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/primary_color"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:src="@drawable/radio"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fm_radio_title"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/fm_radio"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="48sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</de.dlyt.yanndroid.oneui.layout.RoundLinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="15dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<de.dlyt.yanndroid.oneui.layout.RoundLinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@color/primary_color"
|
||||||
|
android:gravity="center|top">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/output_modes"
|
||||||
|
android:layout_width="185dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/output_modes"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="25sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/fm_output"
|
||||||
|
android:layout_width="175dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/wired_earphones"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:theme="@style/ButtonStyle.Colored"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
</de.dlyt.yanndroid.oneui.layout.RoundLinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<de.dlyt.yanndroid.oneui.layout.RoundLinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@color/primary_color"
|
||||||
|
android:gravity="center|top">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/enable_fm"
|
||||||
|
android:layout_width="185dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/turn_on_fm"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="25sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/fmswitch"
|
||||||
|
android:layout_width="175dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/disabled"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:theme="@style/ButtonStyle.Colored" />
|
||||||
|
|
||||||
|
</de.dlyt.yanndroid.oneui.layout.RoundLinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp" />
|
||||||
|
|
||||||
|
<de.dlyt.yanndroid.oneui.layout.RoundLinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="230dp"
|
||||||
|
android:background="@color/primary_color"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center|top">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/freq_title"
|
||||||
|
android:layout_width="375dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/fm_radio_frequency"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="35sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/band_select"
|
||||||
|
android:layout_width="375dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/select_channel"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="25sp"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/secondary_color">
|
||||||
|
|
||||||
|
<de.dlyt.yanndroid.oneui.view.SeekBar
|
||||||
|
android:id="@+id/fm_freq_seekbar"
|
||||||
|
android:layout_width="325dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:progressBackgroundTint="@color/white"
|
||||||
|
android:thumbTint="#263238"
|
||||||
|
android:progressTint="#263238"/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</de.dlyt.yanndroid.oneui.layout.RoundLinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp" />
|
||||||
|
|
||||||
|
<de.dlyt.yanndroid.oneui.layout.RoundLinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@color/primary_color">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="15dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/radio_ctrl"
|
||||||
|
android:layout_width="375dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/radio_controls"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="35sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="15dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/scan_through"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/scan"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:theme="@style/ButtonStyle.Colored"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/stop_scan"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/stop"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:theme="@style/ButtonStyle.Colored" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="15dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|center">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/seek_down"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/seek_down"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:theme="@style/ButtonStyle.Colored"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/quit_fm"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/quit"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:theme="@style/ButtonStyle.Colored"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/seek_up"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:text="@string/seek_up"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:theme="@style/ButtonStyle.Colored" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="15dp" />
|
||||||
|
|
||||||
|
</de.dlyt.yanndroid.oneui.layout.RoundLinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/warning">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/error_img"
|
||||||
|
android:layout_width="75dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
|
android:background="@drawable/ic_samsung_error"
|
||||||
|
android:translationZ="0dp" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/error_simple"
|
||||||
|
android:layout_width="300dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="#00838F"
|
||||||
|
android:textSize="18sp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/error_detailed"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="202dp"
|
||||||
|
android:textColor="#00838F"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/txt_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:textSize="20sp"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/white">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_rewind"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/previous_radio_channel"
|
||||||
|
android:src="@drawable/left"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/play_pause"
|
||||||
|
android:src="@drawable/stop" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_forward"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/next_radio_channel"
|
||||||
|
android:src="@drawable/right"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_close"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:contentDescription="@string/close_radio"
|
||||||
|
android:minWidth="32dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:src="@drawable/power" />
|
||||||
|
</LinearLayout>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
34
universal7885-common/apps/FMRadio/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">FM Radio</string>
|
||||||
|
<string name="fm_radio">FM Radio</string>
|
||||||
|
<string name="output_modes">Output Modes</string>
|
||||||
|
<string name="turn_on_fm">FM Power</string>
|
||||||
|
<string name="disabled">Disabled</string>
|
||||||
|
<string name="enabled">Enabled</string>
|
||||||
|
<string name="phone_speaker">Speaker</string>
|
||||||
|
<string name="wired_earphones">Wired Earphones</string>
|
||||||
|
<string name="select_channel">Radio Channel</string>
|
||||||
|
<string name="refresh">Refresh</string>
|
||||||
|
<string name="seek_down">Seek Down</string>
|
||||||
|
<string name="seek_up">Seek Up</string>
|
||||||
|
<string name="scan">Scan</string>
|
||||||
|
<string name="stop">Stop</string>
|
||||||
|
<string name="mhz">%s Mhz</string>
|
||||||
|
<string name="quit">Quit</string>
|
||||||
|
<string name="album_art">Album Art</string>
|
||||||
|
<string name="previous_radio_channel">Previous Radio Channel</string>
|
||||||
|
<string name="next_radio_channel">Next Radio Channel</string>
|
||||||
|
<string name="play_pause">Play Pause</string>
|
||||||
|
<string name="close_radio">Close Radio</string>
|
||||||
|
<string name="radio_io_error">Failed to open radio chr device!!</string>
|
||||||
|
<string name="radio_io_error_desc">The FM radio app needs to call ioctl()
|
||||||
|
to radio chr device (/dev/radio0). If SELinux is enforcing, try making
|
||||||
|
it permissive</string>
|
||||||
|
<string name="tracks_empty_error">Failed to check if array != null!!</string>
|
||||||
|
<string name="tracks_empty_error_desc">Most likely because FM device couldn\'t
|
||||||
|
find radio channels, if you are sure it should find channels, contact the devs</string>
|
||||||
|
<string name="no_headphones_error">Failed to find wired headset</string>
|
||||||
|
<string name="no_headphones_error_desc">Insert Headphones and try again</string>
|
||||||
|
<string name="radio_controls">Radio Controls</string>
|
||||||
|
<string name="fm_radio_frequency">FM Radio Station</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <mode> 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||