universal7885: FMRadio: Futher improve code

* Using enum classes for various statuses for good readability
* Implement favorite channel saving which was missing
* Use Kotlin coroutines again for refreshing channel list button, for faster UI response
* Added javadoc for better functions description
* Also, code native audio routing system on jni (fm_route.cpp)

Change-Id: I993c88fdb580248d8a55efd8e9b6fed0e36d9b24
Signed-off-by: roynatech2544 <whiteshell2544@naver.com>
This commit is contained in:
roynatech2544 2022-02-28 18:05:41 +09:00
commit 9443bdc315
No known key found for this signature in database
GPG key ID: 9675C32163D88D30
17 changed files with 312 additions and 142 deletions

View file

@ -6,7 +6,7 @@ android_app {
platform_apis: true, platform_apis: true,
privileged: true, privileged: true,
certificate: "platform", certificate: "platform",
jni_libs: ["libfmioctl_jni"], jni_libs: ["libfmnative_jni"],
static_libs: [ static_libs: [
"androidx.core_core", "androidx.core_core",
"androidx.appcompat_appcompat", "androidx.appcompat_appcompat",

View file

@ -21,11 +21,11 @@
android:name=".FMRadioService" android:name=".FMRadioService"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.eurekateam.fmradio.STARTBG" />
<action android:name="com.eurekateam.fmradio.PLAY_FM"/> <action android:name="com.eurekateam.fmradio.PLAY_FM"/>
<action android:name="com.eurekateam.fmradio.NEXT"/> <action android:name="com.eurekateam.fmradio.NEXT"/>
<action android:name="com.eurekateam.fmradio.BEFORE"/> <action android:name="com.eurekateam.fmradio.BEFORE"/>
<action android:name="com.eurekateam.fmradio.QUIT"/> <action android:name="com.eurekateam.fmradio.QUIT"/>
<action android:name="com.eurekateam.fmradio.OUTPUT"/>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</service> </service>

View file

@ -1,9 +1,12 @@
cc_library_shared { cc_library_shared {
name: "libfmioctl_jni", name: "libfmnative_jni",
cflags: [ cflags: [
"-Wno-unused-parameter", "-Wno-unused-parameter",
], ],
srcs: ["fm_ioctl.cpp"], srcs: [
"fm_ioctl.cpp",
"fm_route.cpp",
],
shared_libs: [ shared_libs: [
"libhidlbase", "libhidlbase",
"liblog", "liblog",
@ -11,7 +14,19 @@ cc_library_shared {
"vendor.eureka.hardware.fmradio@1.0", "vendor.eureka.hardware.fmradio@1.0",
"vendor.eureka.hardware.fmradio@1.1", "vendor.eureka.hardware.fmradio@1.1",
"vendor.eureka.hardware.fmradio@1.2", "vendor.eureka.hardware.fmradio@1.2",
"audioflinger-aidl-cpp",
"audiopolicy-aidl-cpp",
"audiopolicy-types-aidl-cpp",
"libaudiofoundation",
"libaudioutils",
"libaudioclient",
"libutils",
"libaudiopolicy",
"libaudiomanager",
],
header_libs: [
"libaudioclient_headers",
"jni_headers",
], ],
header_libs: ["jni_headers"],
} }

View file

@ -0,0 +1,37 @@
/*
* Copyright 2021 Soo Hwan Na "Royna"
*
* 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.
*/
#include <jni.h>
#include <media/IAudioFlinger.h>
#include <media/AudioSystem.h>
#define FM_FAILURE -1
#define FM_SUCCESS 0
#define IOHANDLE 13
using namespace android;
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_eurekateam_fmradio_NativeFMInterface_setAudioRoute
(__unused JNIEnv *env, __unused jobject thiz, jboolean speaker) {
const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
if (af == 0) return PERMISSION_DENIED;
if (speaker){
af->setParameters(IOHANDLE, String8("routing=2"));
}else{
af->setParameters(IOHANDLE, String8("routing=8"));
}
return FM_SUCCESS;
}

View file

@ -3,14 +3,16 @@ package com.eurekateam.fmradio
import android.app.* import android.app.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaMetadata import android.media.MediaMetadata
import android.media.session.MediaSession import android.media.session.MediaSession
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.os.IBinder import android.os.IBinder
import androidx.core.content.res.ResourcesCompat import com.eurekateam.fmradio.enums.OutputState
import android.graphics.BitmapFactory import com.eurekateam.fmradio.enums.PlayState
import com.eurekateam.fmradio.enums.PowerState
import com.eurekateam.fmradio.fragments.MainFragment import com.eurekateam.fmradio.fragments.MainFragment
import com.eurekateam.fmradio.utils.Log import com.eurekateam.fmradio.utils.Log
import java.io.File import java.io.File
@ -20,15 +22,12 @@ class FMRadioService : Service() {
private lateinit var mContext: Context private lateinit var mContext: Context
private lateinit var mAudioManager: AudioManager private lateinit var mAudioManager: AudioManager
private lateinit var mTracks : LongArray 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 mNativeFMInterface: NativeFMInterface
private lateinit var mFilePath : File
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("--- FMRadio Background (IN) ---")
fd = MainFragment.fd fd = MainFragment.fd
mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
mTracks = MainFragment.mTracks mTracks = MainFragment.mTracks
@ -40,62 +39,61 @@ class FMRadioService : Service() {
if (intent != null) { if (intent != null) {
intent.action?.let { Log.i(it) } intent.action?.let { Log.i(it) }
when (intent.action) { when (intent.action) {
BEGIN_BG_SERVICE -> {
mContext = this
mFilePath = mContext.filesDir
mNativeFMInterface = NativeFMInterface()
mTitle = "FM ${mTracks[mIndex].toFloat() / 1000} Mhz"
mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
mIsPlaying = true
}
ACTION_PLAY -> { ACTION_PLAY -> {
mIsPlaying = !mIsPlaying if (mPlayState == PlayState.STATE_PLAYING) {
mTitle = "FM ${mTracks[mIndex].toFloat() / 1000}Mhz" mPlayState = PlayState.STATE_STOPPED
if (mIsPlaying) { mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam)
mAudioManager.setParameters(FM_RADIO_ON) }else if (mPlayState == PlayState.STATE_STOPPED){
} else { mPlayState = PlayState.STATE_PLAYING
mAudioManager.setParameters(FM_RADIO_OFF) mAudioManager.setParameters(PowerState.FM_POWER_ON.mAudioParam)
} }
} }
ACTION_BEFORE -> { ACTION_BEFORE -> {
mIsPlaying = true mPlayState = PlayState.STATE_PLAYING
Log.i("onStartCommand: mCurrentIndex $mIndex") Log.i("mCurrentIndex $mIndex")
if (mIndex > 0) if (mIndex > 0)
mIndex -= 1 mIndex -= 1
if (mIndex >= 1)
mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt()) mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt())
mTitle = "FM ${mTracks[mIndex].toFloat() / 1000}Mhz" MainFragment.mFreqCurrent = mTracks[mIndex].toInt()
} }
ACTION_NEXT -> { ACTION_NEXT -> {
mIsPlaying = true mPlayState = PlayState.STATE_PLAYING
Log.i("onStartCommand: mCurrentIndex $mIndex") Log.i("mCurrentIndex $mIndex")
if (mIndex < mTracks.size - 1) if (mIndex < mTracks.size - 1)
mIndex += 1 mIndex += 1
if (mIndex < mTracks.size - 1)
mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt()) mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt())
mTitle = "FM ${mTracks[mIndex].toFloat() / 1000}Mhz" MainFragment.mFreqCurrent = mTracks[mIndex].toInt()
} }
ACTION_QUIT -> { ACTION_QUIT -> {
mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
mAudioManager.setParameters(FM_RADIO_OFF) Log.i("--- FMRadio Background (OUT) ---")
mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam)
mMediaSession.release() mMediaSession.release()
stopSelf() stopSelf()
} }
else -> { ACTION_OUTPUT -> {
throw IllegalAccessException("Unexpected Intent!") changeOutputDevice(mOutput)
if (mOutput == OutputState.OUTPUT_HEADSET){
mOutput = OutputState.OUTPUT_SPEAKER
}else if (mOutput == OutputState.OUTPUT_SPEAKER){
mOutput = OutputState.OUTPUT_HEADSET
} }
} }
} ACTION_START -> {
mContext = this
mFilePath = mContext.filesDir
mNativeFMInterface = NativeFMInterface()
mTitle = "FM ${mTracks[mIndex].toFloat() / 1000} Mhz"
Log.i("onStartCommand: mTitle=$mTitle")
mMediaSession = MediaSession(this, "FMRadio") mMediaSession = MediaSession(this, "FMRadio")
mNativeFMInterface = NativeFMInterface()
mContext = this
setPlaybackState() setPlaybackState()
sendMetaData("FM ${mTracks[mIndex].toFloat() / 1000}Mhz")
mMediaSession.isActive = true mMediaSession.isActive = true
startForeground(51,pushNotification()) }
}
}
sendMetaData("FM ${mTracks[mIndex].toFloat() / 1000} Mhz")
startForeground(51, pushNotification())
if (mOutput == OutputState.OUTPUT_SPEAKER){
changeOutputDevice(OutputState.OUTPUT_HEADSET)
}
Log.i("--- FMRadio Background (OUT) ---")
return START_STICKY return START_STICKY
} }
private fun setPlaybackState() { private fun setPlaybackState() {
@ -112,7 +110,8 @@ class FMRadioService : Service() {
val metadata = MediaMetadata.Builder() val metadata = MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, mTitle) .putString(MediaMetadata.METADATA_KEY_TITLE, mTitle)
.putString(MediaMetadata.METADATA_KEY_ARTIST, resources.getString(R.string.app_name)) .putString(MediaMetadata.METADATA_KEY_ARTIST, resources.getString(R.string.app_name))
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(mContext.resources, R.drawable.ic_radio)) .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART,
BitmapFactory.decodeResource(mContext.resources, R.drawable.ic_radio))
.build() .build()
mMediaSession.setMetadata(metadata) mMediaSession.setMetadata(metadata)
} }
@ -132,16 +131,31 @@ class FMRadioService : Service() {
builder.setColor(resources.getColor(android.R.color.system_accent1_200, mContext.theme)) builder.setColor(resources.getColor(android.R.color.system_accent1_200, mContext.theme))
builder.setColorized(true) builder.setColorized(true)
builder.style = mediaStyle builder.style = mediaStyle
builder.setOnlyAlertOnce(true)
builder.setChannelId(mContext.packageName) builder.setChannelId(mContext.packageName)
val togglePlay = PendingIntent.getService(mContext, 0, Intent(ACTION_PLAY), PendingIntent.FLAG_IMMUTABLE) val togglePlay = PendingIntent.getService(mContext, 0, Intent(ACTION_PLAY), PendingIntent.FLAG_IMMUTABLE)
val forward = PendingIntent.getService(mContext, 0, Intent(ACTION_NEXT), PendingIntent.FLAG_IMMUTABLE) val forward = PendingIntent.getService(mContext, 0, Intent(ACTION_NEXT), PendingIntent.FLAG_IMMUTABLE)
val rewind = PendingIntent.getService(mContext, 0, Intent(ACTION_BEFORE), PendingIntent.FLAG_IMMUTABLE) val rewind = PendingIntent.getService(mContext, 0, Intent(ACTION_BEFORE), PendingIntent.FLAG_IMMUTABLE)
val close = PendingIntent.getService(mContext, 0, Intent(ACTION_QUIT), PendingIntent.FLAG_IMMUTABLE) val close = PendingIntent.getService(mContext, 0, Intent(ACTION_QUIT), PendingIntent.FLAG_IMMUTABLE)
val output = PendingIntent.getService(mContext, 0, Intent(ACTION_OUTPUT), PendingIntent.FLAG_IMMUTABLE)
val mOutputAction : Notification.Action = Notification.Action.Builder(
when (mOutput) {
OutputState.OUTPUT_HEADSET -> {
Icon.createWithResource(this, R.drawable.ic_volume_up)
}
OutputState.OUTPUT_SPEAKER -> {
Icon.createWithResource(this, R.drawable.ic_headphones)
}
}, "Output Configuration", output
).build()
val mPausePlayAction: Notification.Action = Notification.Action.Builder( val mPausePlayAction: Notification.Action = Notification.Action.Builder(
if (mIsPlaying) { when (mPlayState) {
PlayState.STATE_PLAYING -> {
Icon.createWithResource(this, R.drawable.ic_pause) Icon.createWithResource(this, R.drawable.ic_pause)
}else{ }
PlayState.STATE_STOPPED -> {
Icon.createWithResource(this, R.drawable.ic_play) Icon.createWithResource(this, R.drawable.ic_play)
}
}, "Start/Stop", togglePlay }, "Start/Stop", togglePlay
).build() ).build()
@ -157,6 +171,7 @@ class FMRadioService : Service() {
Icon.createWithResource(this, R.drawable.ic_close), Icon.createWithResource(this, R.drawable.ic_close),
"Close", close "Close", close
).build() ).build()
builder.addAction(mOutputAction)
builder.addAction(mRewindAction) builder.addAction(mRewindAction)
builder.addAction(mPausePlayAction) builder.addAction(mPausePlayAction)
builder.addAction(mForwardAction) builder.addAction(mForwardAction)
@ -165,15 +180,23 @@ class FMRadioService : Service() {
} }
companion object { companion object {
private const val PACKAGENAME = "com.eurekateam.fmradio" private const val PACKAGENAME = "com.eurekateam.fmradio"
private const val BEGIN_BG_SERVICE = "$PACKAGENAME.STARTBG" private const val ACTION_START = "$PACKAGENAME.START"
private const val ACTION_PLAY = "$PACKAGENAME.PLAY_FM" private const val ACTION_PLAY = "$PACKAGENAME.PLAY_FM"
private const val ACTION_NEXT = "$PACKAGENAME.NEXT" private const val ACTION_NEXT = "$PACKAGENAME.NEXT"
private var mIsPlaying = true
private const val ACTION_BEFORE = "$PACKAGENAME.BEFORE" private const val ACTION_BEFORE = "$PACKAGENAME.BEFORE"
private const val ACTION_QUIT = "$PACKAGENAME.QUIT" private const val ACTION_QUIT = "$PACKAGENAME.QUIT"
private const val FM_RADIO_ON = "l_fmradio_mode=on" private const val ACTION_OUTPUT = "$PACKAGENAME.OUTPUT"
private const val FM_RADIO_OFF = "l_fmradio_mode=off" private var mPlayState : PlayState = PlayState.STATE_PLAYING
private const val TAG = "FMRadioService" private var mOutput : OutputState = MainFragment.mHeadset
private var fd : Int = -1
private var mIndex = -1
lateinit var mMediaSession : MediaSession lateinit var mMediaSession : MediaSession
} }
private fun changeOutputDevice(output: OutputState){
if (output == OutputState.OUTPUT_SPEAKER){
mNativeFMInterface.setAudioRoute(false);
}else if (output == OutputState.OUTPUT_HEADSET){
mNativeFMInterface.setAudioRoute(true);
}
}
} }

View file

@ -14,6 +14,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.eurekateam.fmradio.enums.HeadsetState
import com.eurekateam.fmradio.enums.PowerState
import com.eurekateam.fmradio.fragments.ChannelListFragment import com.eurekateam.fmradio.fragments.ChannelListFragment
import com.eurekateam.fmradio.fragments.FavouriteFragment import com.eurekateam.fmradio.fragments.FavouriteFragment
import com.eurekateam.fmradio.fragments.MainFragment import com.eurekateam.fmradio.fragments.MainFragment
@ -25,6 +27,12 @@ import com.google.android.material.textview.MaterialTextView
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var mIntent : Intent private lateinit var mIntent : Intent
/**
* Function to change current [Fragment] to other [Fragment]
* @param [fragment] [Fragment] to change to
* @param [tagFragmentName] Tag for the changing [fragment]
*/
private fun changeFragment(fragment: Fragment?, tagFragmentName: String) { private fun changeFragment(fragment: Fragment?, tagFragmentName: String) {
supportFragmentManager.beginTransaction().apply { supportFragmentManager.beginTransaction().apply {
replace(R.id.container_view, fragment!!, tagFragmentName) replace(R.id.container_view, fragment!!, tagFragmentName)
@ -40,7 +48,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var mAlertImage : AppCompatImageView private lateinit var mAlertImage : AppCompatImageView
private lateinit var mAudioManager : AudioManager private lateinit var mAudioManager : AudioManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
System.loadLibrary("fmioctl_jni") System.loadLibrary("fmnative_jni")
MainFragment.fd = mFMInterface.openFMDevice() MainFragment.fd = mFMInterface.openFMDevice()
mAlertView = (getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater) mAlertView = (getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.alertdialog, null) .inflate(R.layout.alertdialog, null)
@ -65,15 +73,19 @@ class MainActivity : AppCompatActivity() {
} }
DynamicColors.applyToActivitiesIfAvailable(application) DynamicColors.applyToActivitiesIfAvailable(application)
mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager
/**
* Detects whether wired headphones is connected to this device or no
* @see AudioManager.getDevices
*/
val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
for (i in mAudioDeviceInfo.indices){ for (i in mAudioDeviceInfo.indices){
if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET || if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
MainFragment.mHeadSetPlugged = true MainFragment.mHeadSetPlugged = HeadsetState.HEADSET_STATE_CONNECTED
Log.i("Wired Headphones detected") Log.i("Wired Headphones detected")
} }
} }
if (!MainFragment.mHeadSetPlugged){ if (MainFragment.mHeadSetPlugged != HeadsetState.HEADSET_STATE_CONNECTED){
mAlertTitle.text = getString(R.string.no_headphones_error) mAlertTitle.text = getString(R.string.no_headphones_error)
mAlertDesc.text = getString(R.string.no_headphones_error_desc) mAlertDesc.text = getString(R.string.no_headphones_error_desc)
mAlertImage.setImageIcon(Icon.createWithResource(mAlertView.context, mAlertImage.setImageIcon(Icon.createWithResource(mAlertView.context,
@ -83,7 +95,7 @@ class MainActivity : AppCompatActivity() {
.setCancelable(false) .setCancelable(false)
.setView(mAlertView) .setView(mAlertView)
.setNegativeButton(R.string.ok) { v: DialogInterface, _: Int -> .setNegativeButton(R.string.ok) { v: DialogInterface, _: Int ->
mAudioManager.setParameters(FM_RADIO_OFF) mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam)
v.dismiss() v.dismiss()
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
finish() finish()
@ -98,8 +110,6 @@ class MainActivity : AppCompatActivity() {
val mFavouriteFragment = FavouriteFragment() val mFavouriteFragment = FavouriteFragment()
changeFragment(mRadioMainFragment, MainFragment::class.java.name) changeFragment(mRadioMainFragment, MainFragment::class.java.name)
mIntent = Intent(this, FMRadioService::class.java) mIntent = Intent(this, FMRadioService::class.java)
mIntent.action = BEGIN_BG_SERVICE
startService(mIntent)
findViewById<BottomNavigationView>(R.id.bottom_nav_bar).setOnItemSelectedListener { findViewById<BottomNavigationView>(R.id.bottom_nav_bar).setOnItemSelectedListener {
when (it.itemId){ when (it.itemId){
R.id.radio_main -> changeFragment(mRadioMainFragment, MainFragment::class.java.name) R.id.radio_main -> changeFragment(mRadioMainFragment, MainFragment::class.java.name)
@ -110,18 +120,21 @@ class MainActivity : AppCompatActivity() {
} }
} }
/**
* Register a [BroadcastReceiver] for listening to headset events
*/
private val mWiredHeadsetReceiver: BroadcastReceiver = object : BroadcastReceiver() { private val mWiredHeadsetReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AudioManager.ACTION_HEADSET_PLUG) { if (intent.action == AudioManager.ACTION_HEADSET_PLUG) {
MainFragment.mHeadSetPlugged = false MainFragment.mHeadSetPlugged = HeadsetState.HEADSET_STATE_DISCONNECTED
val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) val mAudioDeviceInfo = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
for (i in mAudioDeviceInfo.indices){ for (i in mAudioDeviceInfo.indices){
if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET || if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES){ mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES){
MainFragment.mHeadSetPlugged = true MainFragment.mHeadSetPlugged = HeadsetState.HEADSET_STATE_CONNECTED
} }
} }
if (!MainFragment.mHeadSetPlugged){ if (MainFragment.mHeadSetPlugged != HeadsetState.HEADSET_STATE_CONNECTED){
Log.w("onReceive: Headset Unplugged") Log.w("onReceive: Headset Unplugged")
mAlertTitle.text = getString(R.string.no_headphones_error) mAlertTitle.text = getString(R.string.no_headphones_error)
mAlertDesc.text = getString(R.string.no_headphones_error_desc) mAlertDesc.text = getString(R.string.no_headphones_error_desc)
@ -134,7 +147,7 @@ class MainActivity : AppCompatActivity() {
.setView(mAlertView) .setView(mAlertView)
.setNegativeButton(R.string.ok) { v: DialogInterface, _: Int -> .setNegativeButton(R.string.ok) { v: DialogInterface, _: Int ->
v.dismiss() v.dismiss()
mAudioManager.setParameters(FM_RADIO_OFF) mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam)
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
finish() finish()
},500) },500)
@ -144,13 +157,19 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
/**
* Helper function to get [MainActivity]'s [FragmentManager] to sub [Fragment]
* @return [FragmentManager] supplied to this activity
*/
fun getMySupportFragmentManager(): FragmentManager { fun getMySupportFragmentManager(): FragmentManager {
return supportFragmentManager return supportFragmentManager
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
mIntent.action = BEGIN_BG_SERVICE Log.i("Application onPause")
mIntent.action = ACTION_START
startService(mIntent) startService(mIntent)
} }
@ -158,10 +177,7 @@ class MainActivity : AppCompatActivity() {
super.onRestart() super.onRestart()
stopService(mIntent) stopService(mIntent)
} }
companion object { companion object {
private const val PACKAGENAME = "com.eurekateam.fmradio" private const val ACTION_START = "com.eurekateam.fmradio.START"
private const val BEGIN_BG_SERVICE = "$PACKAGENAME.STARTBG"
private const val FM_RADIO_OFF = "l_fmradio_mode=off"
} }
} }

View file

@ -21,4 +21,5 @@ class NativeFMInterface {
external fun setFMRSSI(fd: Int, rssi: Long): Int external fun setFMRSSI(fd: Int, rssi: Long): Int
external fun closeFMDevice(fd: Int) external fun closeFMDevice(fd: Int)
external fun getSysfsSupport(): Boolean external fun getSysfsSupport(): Boolean
external fun setAudioRoute(speaker: Boolean): Int
} }

View file

@ -10,6 +10,7 @@ import com.eurekateam.fmradio.NativeFMInterface
import com.eurekateam.fmradio.PebbleTextView import com.eurekateam.fmradio.PebbleTextView
import com.eurekateam.fmradio.R import com.eurekateam.fmradio.R
import com.eurekateam.fmradio.fragments.MainFragment import com.eurekateam.fmradio.fragments.MainFragment
import com.eurekateam.fmradio.utils.FileUtilities
class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() { class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() {
@ -21,6 +22,11 @@ class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() {
} }
} }
mFavoriteList.sort() mFavoriteList.sort()
var mData = ""
for (i in mFavoriteList){
mData += "$i\n"
}
FileUtilities.writeToFile(FileUtilities.mFavouriteChannelFileName, mData, mContext)
} }
override fun getCount(): Int { override fun getCount(): Int {
return mFavoriteList.size return mFavoriteList.size
@ -47,6 +53,10 @@ class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() {
setOnClickListener { setOnClickListener {
mFMInterface.setFMFreq(MainFragment.fd, mFavoriteList[id]) mFMInterface.setFMFreq(MainFragment.fd, mFavoriteList[id])
MainFragment.mFreqCurrent = mFavoriteList[id] MainFragment.mFreqCurrent = mFavoriteList[id]
FileUtilities.writeToFile(
FileUtilities.mFMFreqFileName,
MainFragment.mFreqCurrent.toString(), mContext
)
} }
} }
return mAnotherConvertView return mAnotherConvertView

View file

@ -0,0 +1,11 @@
package com.eurekateam.fmradio.enums
/**
* [HEADSET_STATE_CONNECTED] Headset is connected
*
* [HEADSET_STATE_DISCONNECTED] Headset is disconnected
*/
enum class HeadsetState {
HEADSET_STATE_CONNECTED,
HEADSET_STATE_DISCONNECTED,
}

View file

@ -0,0 +1,11 @@
package com.eurekateam.fmradio.enums
/***
* [OUTPUT_SPEAKER] : Output to speaker
*
* [OUTPUT_HEADSET] : Output to headset
*/
enum class OutputState {
OUTPUT_SPEAKER,
OUTPUT_HEADSET
}

View file

@ -0,0 +1,11 @@
package com.eurekateam.fmradio.enums
/***
* [STATE_PLAYING] : Currently playing
*
* [STATE_STOPPED] : Stopped playing
*/
enum class PlayState {
STATE_PLAYING,
STATE_STOPPED
}

View file

@ -0,0 +1,13 @@
package com.eurekateam.fmradio.enums
/**
* [FM_POWER_OFF] : Turns off fm audio
*
* [FM_POWER_ON] : Turns on fm audio
*
* This enum includes audio parameter that can be written to A-BOX
*/
enum class PowerState(val mAudioParam: String) {
FM_POWER_ON("l_fmradio_mode=on"),
FM_POWER_OFF("l_fmradio_mode=off")
}

View file

@ -12,6 +12,10 @@ import com.eurekateam.fmradio.MainActivity
import com.eurekateam.fmradio.R import com.eurekateam.fmradio.R
import com.eurekateam.fmradio.adapters.ListViewAdapter import com.eurekateam.fmradio.adapters.ListViewAdapter
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ChannelListFragment : Fragment(R.layout.activity_channel_list), class ChannelListFragment : Fragment(R.layout.activity_channel_list),
View.OnClickListener { View.OnClickListener {
@ -45,11 +49,17 @@ class ChannelListFragment : Fragment(R.layout.activity_channel_list),
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
GlobalScope.launch {
withContext(Dispatchers.IO){
MainFragment.mRefreshTracks() MainFragment.mRefreshTracks()
}
withContext(Dispatchers.Main){
(requireActivity() as MainActivity).getMySupportFragmentManager().apply { (requireActivity() as MainActivity).getMySupportFragmentManager().apply {
beginTransaction().remove(this@ChannelListFragment).commit() beginTransaction().remove(this@ChannelListFragment).commit()
executePendingTransactions() executePendingTransactions()
beginTransaction().add(R.id.container_view, this@ChannelListFragment).commit() beginTransaction().add(R.id.container_view, this@ChannelListFragment).commit()
} }
} }
}
}
} }

View file

@ -1,25 +1,27 @@
package com.eurekateam.fmradio.fragments package com.eurekateam.fmradio.fragments
import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.media.AudioManager import android.media.AudioManager
import android.os.Bundle import android.os.Bundle
import android.view.Display
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatSeekBar import androidx.appcompat.widget.AppCompatSeekBar
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.eurekateam.fmradio.utils.FileUtilities
import com.eurekateam.fmradio.utils.Log
import com.eurekateam.fmradio.NativeFMInterface import com.eurekateam.fmradio.NativeFMInterface
import com.eurekateam.fmradio.R import com.eurekateam.fmradio.R
import com.eurekateam.fmradio.enums.HeadsetState
import com.eurekateam.fmradio.enums.OutputState
import com.eurekateam.fmradio.enums.PowerState
import com.eurekateam.fmradio.utils.FileUtilities
import com.eurekateam.fmradio.utils.Log
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -56,7 +58,7 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
R.drawable.ic_star, requireContext().theme)!! R.drawable.ic_star, requireContext().theme)!!
mStarFilled = ResourcesCompat.getDrawable(requireContext().resources, mStarFilled = ResourcesCompat.getDrawable(requireContext().resources,
R.drawable.ic_star_filled, requireContext().theme)!! R.drawable.ic_star_filled, requireContext().theme)!!
mAudioManager = requireContext().getSystemService(AppCompatActivity.AUDIO_SERVICE) as AudioManager mAudioManager = requireContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager
mVolumeUp = mRootView.findViewById(R.id.volume_up) mVolumeUp = mRootView.findViewById(R.id.volume_up)
mVolumeDown = mRootView.findViewById(R.id.volume_down) mVolumeDown = mRootView.findViewById(R.id.volume_down)
mSeekBar = mRootView.findViewById(R.id.volume_seekbar) mSeekBar = mRootView.findViewById(R.id.volume_seekbar)
@ -97,15 +99,24 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
else else
setBackgroundColor(resources.getColor(android.R.color.system_accent1_700, requireContext().theme)) setBackgroundColor(resources.getColor(android.R.color.system_accent1_700, requireContext().theme))
} }
var mFMInit = false
GlobalScope.launch { GlobalScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
if (FileUtilities.checkIfExistFile(FileUtilities.mFavouriteChannelFileName, requireContext())) {
val mFavData = FileUtilities.readFromFile(
FileUtilities.mFavouriteChannelFileName, requireContext()
)
for (mItem in mFavData.split("\\r?\\n".toRegex())) {
if (mItem.isNotBlank())
mFavStats[mItem.toInt()] = true
}
}
var mMute = false
if (mFreqCurrent == -1) { if (mFreqCurrent == -1) {
mAudioManager.setParameters(FM_RADIO_OFF) mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
mUpdateEnableDisable(false, mRootView) mUpdateEnableDisable(false, mRootView)
} }
mFMInit = true mMute = true
} }
if (FileUtilities.checkIfExistFile(FileUtilities.mFMFreqFileName, requireContext())){ if (FileUtilities.checkIfExistFile(FileUtilities.mFMFreqFileName, requireContext())){
@ -141,27 +152,8 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
} }
} }
} }
withContext(Dispatchers.IO){ if (!mMute)
if (FileUtilities.checkIfExistFile(FileUtilities.mHeadsetFileName, requireContext())){ mFMInterface.setFMMute(fd, true)
mHeadset = true
mAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
mAudioManager.isSpeakerphoneOn = mHeadset
mAudioManager.setParameters(FM_RADIO_OFF)
mAudioManager.setParameters(FM_RADIO_ON)
mAudioManager.mode = AudioManager.MODE_NORMAL
withContext(Dispatchers.Main){
if (mHeadset) {
mOutputSwitch.setImageIcon(
Icon.createWithResource(
requireContext(),
R.drawable.ic_volume_up
)
)
FileUtilities.createFile(FileUtilities.mHeadsetFileName, requireContext())
}
}
}
}
mFMInterface.setFMFreq(fd, mFMInterface.getFMLower(fd)) mFMInterface.setFMFreq(fd, mFMInterface.getFMLower(fd))
mRefreshTracks() mRefreshTracks()
if (mFreqCurrent != -1){ if (mFreqCurrent != -1){
@ -170,14 +162,13 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
mFreqCurrent = mFMInterface.getFMLower(fd) mFreqCurrent = mFMInterface.getFMLower(fd)
} }
mFMInterface.setFMBoot(fd) mFMInterface.setFMBoot(fd)
if (!mFMInit && isAdded) {
mFreqCurrent = mFMInterface.getFMFreq(fd).toInt() mFreqCurrent = mFMInterface.getFMFreq(fd).toInt()
mFMInterface.setFMFreq(fd, mFreqCurrent) mFMInterface.setFMFreq(fd, mFreqCurrent)
requireActivity().mainExecutor.execute { withContext(Dispatchers.Main) {
mFMFreq.text = mCleanFormat.format( mFreqCurrent.toFloat() / 1000) mFMFreq.text = mCleanFormat.format( mFreqCurrent.toFloat() / 1000)
} }
} if (!mMute)
if (mTracks.isEmpty()) mFMInterface.setFMMute(fd, false)
if (mFreqCurrent == -1) if (mFreqCurrent == -1)
mFMInterface.setFMThread(fd, true) mFMInterface.setFMThread(fd, true)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
@ -196,14 +187,34 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
} }
return mRootView return mRootView
} }
/**
* Extension function for [View], for Grayed out, disabled View
* @param mTextView Whether the target view is touchable [FloatingActionButton] or
* [MaterialTextView] with useless touch attr
*/
private fun View.disable(mTextView : Boolean = false) { private fun View.disable(mTextView : Boolean = false) {
alpha = .7f alpha = .7f
if(!mTextView) isEnabled = false if(!mTextView) isEnabled = false
} }
/**
* Extension function for [View], for reverting Grayed out, disabled View
* @param mTextView Whether the target view is touchable [FloatingActionButton] or
* [MaterialTextView] with useless touch attr
*/
private fun View.enable(mTextView : Boolean = false) { private fun View.enable(mTextView : Boolean = false) {
alpha = 1.0f alpha = 1.0f
if(!mTextView) isEnabled = true if(!mTextView) isEnabled = true
} }
/**
* Uses array of [R] to make the [View] look disabled or enabled
* @param mEnabled Should we enable the Views or not
* @param mView The root view we should find views such as [FloatingActionButton].
* Defaults to [requireView]
* @see [enable]
* @see [disable]
*/
private fun mUpdateEnableDisable(mEnabled: Boolean, mView: View = requireView()){ private fun mUpdateEnableDisable(mEnabled: Boolean, mView: View = requireView()){
val mTextViewList = listOf(R.id.fm_freq, R.id.freq_misc) val mTextViewList = listOf(R.id.fm_freq, R.id.freq_misc)
val mFloatButtonList = listOf( val mFloatButtonList = listOf(
@ -227,29 +238,30 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
mOutputSwitch.id -> { mOutputSwitch.id -> {
mAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION if (mHeadset == OutputState.OUTPUT_HEADSET) {
mAudioManager.isSpeakerphoneOn = mHeadset
mAudioManager.setParameters(FM_RADIO_OFF)
mAudioManager.setParameters(FM_RADIO_ON)
mAudioManager.mode = AudioManager.MODE_NORMAL
if (mHeadset) {
mOutputSwitch.setImageIcon( mOutputSwitch.setImageIcon(
Icon.createWithResource( Icon.createWithResource(
requireContext(), requireContext(),
R.drawable.ic_volume_up R.drawable.ic_volume_up
) )
) )
FileUtilities.createFile(FileUtilities.mHeadsetFileName, requireContext()) val ret = mFMInterface.setAudioRoute(true);
}else{ Log.i("mFMInterface.setAudioRoute return $ret");
}else if (mHeadset == OutputState.OUTPUT_SPEAKER){
mOutputSwitch.setImageIcon( mOutputSwitch.setImageIcon(
Icon.createWithResource( Icon.createWithResource(
requireContext(), requireContext(),
R.drawable.ic_headphones R.drawable.ic_headphones
) )
) )
FileUtilities.removeFile(FileUtilities.mHeadsetFileName, requireContext()) val ret = mFMInterface.setAudioRoute(false);
Log.i("mFMInterface.setAudioRoute return $ret");
}
if (mHeadset == OutputState.OUTPUT_HEADSET){
mHeadset = OutputState.OUTPUT_SPEAKER
}else if (mHeadset == OutputState.OUTPUT_SPEAKER){
mHeadset = OutputState.OUTPUT_HEADSET
} }
mHeadset = !mHeadset
} }
mVolumeUp.id -> { mVolumeUp.id -> {
if (mVolume < 15) if (mVolume < 15)
@ -302,10 +314,10 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
} }
mPowerBtn.id -> { mPowerBtn.id -> {
if (mFMPower){ if (mFMPower){
mAudioManager.setParameters(FM_RADIO_OFF) mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam)
mFMFreq.text = getText(R.string.inital_freq) mFMFreq.text = getText(R.string.inital_freq)
}else{ }else{
mAudioManager.setParameters(FM_RADIO_ON) mAudioManager.setParameters(PowerState.FM_POWER_ON.mAudioParam)
mFMFreq.text = mCleanFormat.format(mFreqCurrent.toFloat() / 1000) mFMFreq.text = mCleanFormat.format(mFreqCurrent.toFloat() / 1000)
} }
mUpdateEnableDisable(!mFMPower) mUpdateEnableDisable(!mFMPower)
@ -372,13 +384,11 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
companion object { companion object {
private var mVolume = -1 private var mVolume = -1
var fd = -1 var fd = -1
private var mHeadset = false var mHeadset = OutputState.OUTPUT_SPEAKER
var mFreqCurrent = -1 var mFreqCurrent = -1
private var mFMPower = false private var mFMPower = false
var mHeadSetPlugged = false var mHeadSetPlugged : HeadsetState = HeadsetState.HEADSET_STATE_DISCONNECTED
var mTracks : LongArray = emptyArray<Long>().toLongArray() var mTracks : LongArray = emptyArray<Long>().toLongArray()
private const val FM_RADIO_ON = "l_fmradio_mode=on"
private const val FM_RADIO_OFF = "l_fmradio_mode=off"
fun mRefreshTracks(){ fun mRefreshTracks(){
NativeFMInterface().setFMFreq(fd, NativeFMInterface().getFMLower(fd)) NativeFMInterface().setFMFreq(fd, NativeFMInterface().getFMLower(fd))
mTracks = NativeFMInterface().getFMTracks(fd) mTracks = NativeFMInterface().getFMTracks(fd)
@ -391,9 +401,18 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener,
val mFavStats = HashMap<Int, Boolean>(30) val mFavStats = HashMap<Int, Boolean>(30)
} }
/**
* Helper function to remove zero values on a [LongArray].
* Since we don't know the channel count, we initialize it with size 30.
* But most likely the channel count is less then 30, so remaining values
* are filled with zero.
*
* @param array The target Long array
* @return Long array with zeros removed
*/
private fun removeZeros(array : LongArray): LongArray{ private fun removeZeros(array : LongArray): LongArray{
val mArray : MutableList<Long> = emptyList<Long>().toMutableList() val mArray : MutableList<Long> = emptyList<Long>().toMutableList()
Log.d("removeZeros: Got array") Log.d("removeZeros: Got array size ${array.size}")
for (i in array.indices){ for (i in array.indices){
if(array[i] > 0L){ if(array[i] > 0L){
mArray.add(array[i]) mArray.add(array[i])

View file

@ -6,7 +6,7 @@ import java.io.*
object FileUtilities { object FileUtilities {
const val mFMFreqFileName = "fm_freq_current" const val mFMFreqFileName = "fm_freq_current"
const val mFMVolumeFileName = "fm_volume_current" const val mFMVolumeFileName = "fm_volume_current"
const val mHeadsetFileName = "fm_using_headset" const val mFavouriteChannelFileName = "fm_fav_freqs"
fun writeToFile(fileName : String, data: String, mContext: Context){ fun writeToFile(fileName : String, data: String, mContext: Context){
var os: OutputStream? = null var os: OutputStream? = null
@ -41,12 +41,7 @@ object FileUtilities {
} }
return String(mByteArray) return String(mByteArray)
} }
fun createFile(fileName: String, mContext: Context){
File(mContext.filesDir.absolutePath + "/" + fileName).createNewFile()
}
fun removeFile(fileName: String, mContext: Context){
File(mContext.filesDir.absolutePath + "/" + fileName).delete()
}
fun checkIfExistFile(fileName: String, mContext: Context): Boolean { fun checkIfExistFile(fileName: String, mContext: Context): Boolean {
return File(mContext.filesDir.absolutePath + "/" + fileName).exists() return File(mContext.filesDir.absolutePath + "/" + fileName).exists()
} }

View file

@ -4,7 +4,6 @@ android_app {
"src/**/*.kt", "src/**/*.kt",
], ],
platform_apis: true, platform_apis: true,
privileged: true,
certificate: "platform", certificate: "platform",
jni_libs: ["libsamsungparts_jni"], jni_libs: ["libsamsungparts_jni"],
static_libs: ["androidx.preference_preference"], static_libs: ["androidx.preference_preference"],

View file

@ -2,4 +2,3 @@
### DATA ### DATA
/data/zram(/.*)? u:object_r:zram_data_file:s0 /data/zram(/.*)? u:object_r:zram_data_file:s0