diff --git a/universal7885-common/apps/FMRadio/Android.bp b/universal7885-common/apps/FMRadio/Android.bp index d990883..25046db 100644 --- a/universal7885-common/apps/FMRadio/Android.bp +++ b/universal7885-common/apps/FMRadio/Android.bp @@ -6,7 +6,7 @@ android_app { platform_apis: true, privileged: true, certificate: "platform", - jni_libs: ["libfmioctl_jni"], + jni_libs: ["libfmnative_jni"], static_libs: [ "androidx.core_core", "androidx.appcompat_appcompat", diff --git a/universal7885-common/apps/FMRadio/AndroidManifest.xml b/universal7885-common/apps/FMRadio/AndroidManifest.xml index 3c9c98b..fda923d 100644 --- a/universal7885-common/apps/FMRadio/AndroidManifest.xml +++ b/universal7885-common/apps/FMRadio/AndroidManifest.xml @@ -21,11 +21,11 @@ android:name=".FMRadioService" android:exported="false"> - + diff --git a/universal7885-common/apps/FMRadio/jni/Android.bp b/universal7885-common/apps/FMRadio/jni/Android.bp index 56e7e41..16652d5 100644 --- a/universal7885-common/apps/FMRadio/jni/Android.bp +++ b/universal7885-common/apps/FMRadio/jni/Android.bp @@ -1,9 +1,12 @@ cc_library_shared { - name: "libfmioctl_jni", + name: "libfmnative_jni", cflags: [ "-Wno-unused-parameter", ], - srcs: ["fm_ioctl.cpp"], + srcs: [ + "fm_ioctl.cpp", + "fm_route.cpp", + ], shared_libs: [ "libhidlbase", "liblog", @@ -11,7 +14,19 @@ cc_library_shared { "vendor.eureka.hardware.fmradio@1.0", "vendor.eureka.hardware.fmradio@1.1", "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"], } diff --git a/universal7885-common/apps/FMRadio/jni/fm_route.cpp b/universal7885-common/apps/FMRadio/jni/fm_route.cpp new file mode 100644 index 0000000..c3b1b3c --- /dev/null +++ b/universal7885-common/apps/FMRadio/jni/fm_route.cpp @@ -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 +#include +#include + +#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& 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; +} diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt index 9eaf698..b3632bf 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/FMRadioService.kt @@ -3,14 +3,16 @@ package com.eurekateam.fmradio import android.app.* import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import android.graphics.drawable.Icon import android.media.AudioManager import android.media.MediaMetadata import android.media.session.MediaSession import android.media.session.PlaybackState import android.os.IBinder -import androidx.core.content.res.ResourcesCompat -import android.graphics.BitmapFactory +import com.eurekateam.fmradio.enums.OutputState +import com.eurekateam.fmradio.enums.PlayState +import com.eurekateam.fmradio.enums.PowerState import com.eurekateam.fmradio.fragments.MainFragment import com.eurekateam.fmradio.utils.Log import java.io.File @@ -20,15 +22,12 @@ class FMRadioService : Service() { private lateinit var mContext: Context private lateinit var mAudioManager: AudioManager 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 mFilePath : File override fun onBind(intent: Intent?): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.i("--- FMRadio Background (IN) ---") fd = MainFragment.fd mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager mTracks = MainFragment.mTracks @@ -40,62 +39,61 @@ class FMRadioService : Service() { if (intent != null) { intent.action?.let { Log.i(it) } 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 -> { - mIsPlaying = !mIsPlaying - mTitle = "FM ${mTracks[mIndex].toFloat() / 1000}Mhz" - if (mIsPlaying) { - mAudioManager.setParameters(FM_RADIO_ON) - } else { - mAudioManager.setParameters(FM_RADIO_OFF) + if (mPlayState == PlayState.STATE_PLAYING) { + mPlayState = PlayState.STATE_STOPPED + mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam) + }else if (mPlayState == PlayState.STATE_STOPPED){ + mPlayState = PlayState.STATE_PLAYING + mAudioManager.setParameters(PowerState.FM_POWER_ON.mAudioParam) } } ACTION_BEFORE -> { - mIsPlaying = true - Log.i("onStartCommand: mCurrentIndex $mIndex") + mPlayState = PlayState.STATE_PLAYING + Log.i("mCurrentIndex $mIndex") if (mIndex > 0) mIndex -= 1 - if (mIndex >= 1) - mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt()) - mTitle = "FM ${mTracks[mIndex].toFloat() / 1000}Mhz" + mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt()) + MainFragment.mFreqCurrent = mTracks[mIndex].toInt() } ACTION_NEXT -> { - mIsPlaying = true - Log.i("onStartCommand: mCurrentIndex $mIndex") + mPlayState = PlayState.STATE_PLAYING + Log.i("mCurrentIndex $mIndex") if (mIndex < mTracks.size - 1) mIndex += 1 - if (mIndex < mTracks.size - 1) - mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt()) - mTitle = "FM ${mTracks[mIndex].toFloat() / 1000}Mhz" + mNativeFMInterface.setFMFreq(fd, mTracks[mIndex].toInt()) + MainFragment.mFreqCurrent = mTracks[mIndex].toInt() } ACTION_QUIT -> { 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() stopSelf() } - else -> { - throw IllegalAccessException("Unexpected Intent!") + ACTION_OUTPUT -> { + changeOutputDevice(mOutput) + if (mOutput == OutputState.OUTPUT_HEADSET){ + mOutput = OutputState.OUTPUT_SPEAKER + }else if (mOutput == OutputState.OUTPUT_SPEAKER){ + mOutput = OutputState.OUTPUT_HEADSET + } + } + ACTION_START -> { + mMediaSession = MediaSession(this, "FMRadio") + mNativeFMInterface = NativeFMInterface() + mContext = this + setPlaybackState() + mMediaSession.isActive = true } } } - mContext = this - mFilePath = mContext.filesDir - mNativeFMInterface = NativeFMInterface() - mTitle = "FM ${mTracks[mIndex].toFloat() / 1000} Mhz" - Log.i("onStartCommand: mTitle=$mTitle") - mMediaSession = MediaSession(this, "FMRadio") - setPlaybackState() - sendMetaData("FM ${mTracks[mIndex].toFloat() / 1000}Mhz") - 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 } private fun setPlaybackState() { @@ -112,7 +110,8 @@ class FMRadioService : Service() { val metadata = MediaMetadata.Builder() .putString(MediaMetadata.METADATA_KEY_TITLE, mTitle) .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() mMediaSession.setMetadata(metadata) } @@ -132,16 +131,31 @@ class FMRadioService : Service() { builder.setColor(resources.getColor(android.R.color.system_accent1_200, mContext.theme)) builder.setColorized(true) builder.style = mediaStyle + builder.setOnlyAlertOnce(true) builder.setChannelId(mContext.packageName) 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 rewind = PendingIntent.getService(mContext, 0, Intent(ACTION_BEFORE), 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( - if (mIsPlaying) { - Icon.createWithResource(this, R.drawable.ic_pause) - }else{ - Icon.createWithResource(this, R.drawable.ic_play) + when (mPlayState) { + PlayState.STATE_PLAYING -> { + Icon.createWithResource(this, R.drawable.ic_pause) + } + PlayState.STATE_STOPPED -> { + Icon.createWithResource(this, R.drawable.ic_play) + } }, "Start/Stop", togglePlay ).build() @@ -157,6 +171,7 @@ class FMRadioService : Service() { Icon.createWithResource(this, R.drawable.ic_close), "Close", close ).build() + builder.addAction(mOutputAction) builder.addAction(mRewindAction) builder.addAction(mPausePlayAction) builder.addAction(mForwardAction) @@ -165,15 +180,23 @@ class FMRadioService : Service() { } companion object { 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_NEXT = "$PACKAGENAME.NEXT" - private var mIsPlaying = true 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 ACTION_OUTPUT = "$PACKAGENAME.OUTPUT" + private var mPlayState : PlayState = PlayState.STATE_PLAYING + private var mOutput : OutputState = MainFragment.mHeadset + private var fd : Int = -1 + private var mIndex = -1 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); + } + } } diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt index 3d0d9b2..66aa543 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/MainActivity.kt @@ -14,6 +14,8 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatImageView import androidx.fragment.app.Fragment 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.FavouriteFragment import com.eurekateam.fmradio.fragments.MainFragment @@ -25,6 +27,12 @@ import com.google.android.material.textview.MaterialTextView class MainActivity : AppCompatActivity() { 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) { supportFragmentManager.beginTransaction().apply { replace(R.id.container_view, fragment!!, tagFragmentName) @@ -40,7 +48,7 @@ class MainActivity : AppCompatActivity() { private lateinit var mAlertImage : AppCompatImageView private lateinit var mAudioManager : AudioManager override fun onCreate(savedInstanceState: Bundle?) { - System.loadLibrary("fmioctl_jni") + System.loadLibrary("fmnative_jni") MainFragment.fd = mFMInterface.openFMDevice() mAlertView = (getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater) .inflate(R.layout.alertdialog, null) @@ -65,15 +73,19 @@ class MainActivity : AppCompatActivity() { } DynamicColors.applyToActivitiesIfAvailable(application) 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) for (i in mAudioDeviceInfo.indices){ if (mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADSET || mAudioDeviceInfo[i].type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { - MainFragment.mHeadSetPlugged = true + MainFragment.mHeadSetPlugged = HeadsetState.HEADSET_STATE_CONNECTED Log.i("Wired Headphones detected") } } - if (!MainFragment.mHeadSetPlugged){ + if (MainFragment.mHeadSetPlugged != HeadsetState.HEADSET_STATE_CONNECTED){ mAlertTitle.text = getString(R.string.no_headphones_error) mAlertDesc.text = getString(R.string.no_headphones_error_desc) mAlertImage.setImageIcon(Icon.createWithResource(mAlertView.context, @@ -83,7 +95,7 @@ class MainActivity : AppCompatActivity() { .setCancelable(false) .setView(mAlertView) .setNegativeButton(R.string.ok) { v: DialogInterface, _: Int -> - mAudioManager.setParameters(FM_RADIO_OFF) + mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam) v.dismiss() Handler(Looper.getMainLooper()).postDelayed({ finish() @@ -98,8 +110,6 @@ class MainActivity : AppCompatActivity() { val mFavouriteFragment = FavouriteFragment() changeFragment(mRadioMainFragment, MainFragment::class.java.name) mIntent = Intent(this, FMRadioService::class.java) - mIntent.action = BEGIN_BG_SERVICE - startService(mIntent) findViewById(R.id.bottom_nav_bar).setOnItemSelectedListener { when (it.itemId){ 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() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == AudioManager.ACTION_HEADSET_PLUG) { - MainFragment.mHeadSetPlugged = false + MainFragment.mHeadSetPlugged = HeadsetState.HEADSET_STATE_DISCONNECTED 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){ - MainFragment.mHeadSetPlugged = true + MainFragment.mHeadSetPlugged = HeadsetState.HEADSET_STATE_CONNECTED } } - if (!MainFragment.mHeadSetPlugged){ + if (MainFragment.mHeadSetPlugged != HeadsetState.HEADSET_STATE_CONNECTED){ Log.w("onReceive: Headset Unplugged") mAlertTitle.text = getString(R.string.no_headphones_error) mAlertDesc.text = getString(R.string.no_headphones_error_desc) @@ -134,7 +147,7 @@ class MainActivity : AppCompatActivity() { .setView(mAlertView) .setNegativeButton(R.string.ok) { v: DialogInterface, _: Int -> v.dismiss() - mAudioManager.setParameters(FM_RADIO_OFF) + mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam) Handler(Looper.getMainLooper()).postDelayed({ finish() },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 { return supportFragmentManager } override fun onPause() { super.onPause() - mIntent.action = BEGIN_BG_SERVICE + Log.i("Application onPause") + mIntent.action = ACTION_START startService(mIntent) } @@ -158,10 +177,7 @@ class MainActivity : AppCompatActivity() { super.onRestart() stopService(mIntent) } - companion object { - private const val PACKAGENAME = "com.eurekateam.fmradio" - private const val BEGIN_BG_SERVICE = "$PACKAGENAME.STARTBG" - private const val FM_RADIO_OFF = "l_fmradio_mode=off" + private const val ACTION_START = "com.eurekateam.fmradio.START" } } diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt index 2d405f5..11797a9 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/NativeFMInterface.kt @@ -21,4 +21,5 @@ class NativeFMInterface { external fun setFMRSSI(fd: Int, rssi: Long): Int external fun closeFMDevice(fd: Int) external fun getSysfsSupport(): Boolean -} \ No newline at end of file + external fun setAudioRoute(speaker: Boolean): Int +} diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/adapters/PebbleLayoutAdapter.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/adapters/PebbleLayoutAdapter.kt index d033531..e32fb14 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/adapters/PebbleLayoutAdapter.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/adapters/PebbleLayoutAdapter.kt @@ -10,6 +10,7 @@ import com.eurekateam.fmradio.NativeFMInterface import com.eurekateam.fmradio.PebbleTextView import com.eurekateam.fmradio.R import com.eurekateam.fmradio.fragments.MainFragment +import com.eurekateam.fmradio.utils.FileUtilities class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() { @@ -21,6 +22,11 @@ class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() { } } mFavoriteList.sort() + var mData = "" + for (i in mFavoriteList){ + mData += "$i\n" + } + FileUtilities.writeToFile(FileUtilities.mFavouriteChannelFileName, mData, mContext) } override fun getCount(): Int { return mFavoriteList.size @@ -47,6 +53,10 @@ class PebbleLayoutAdapter (private val mContext: Context) : BaseAdapter() { setOnClickListener { mFMInterface.setFMFreq(MainFragment.fd, mFavoriteList[id]) MainFragment.mFreqCurrent = mFavoriteList[id] + FileUtilities.writeToFile( + FileUtilities.mFMFreqFileName, + MainFragment.mFreqCurrent.toString(), mContext + ) } } return mAnotherConvertView diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/HeadsetState.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/HeadsetState.kt new file mode 100644 index 0000000..8323743 --- /dev/null +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/HeadsetState.kt @@ -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, +} \ No newline at end of file diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/OutputState.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/OutputState.kt new file mode 100644 index 0000000..d632149 --- /dev/null +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/OutputState.kt @@ -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 +} \ No newline at end of file diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/PlayState.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/PlayState.kt new file mode 100644 index 0000000..291fee7 --- /dev/null +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/PlayState.kt @@ -0,0 +1,11 @@ +package com.eurekateam.fmradio.enums + +/*** + * [STATE_PLAYING] : Currently playing + * + * [STATE_STOPPED] : Stopped playing + */ +enum class PlayState { + STATE_PLAYING, + STATE_STOPPED +} \ No newline at end of file diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/PowerState.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/PowerState.kt new file mode 100644 index 0000000..2b1fff9 --- /dev/null +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/enums/PowerState.kt @@ -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") +} \ No newline at end of file diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/ChannelListFragment.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/ChannelListFragment.kt index a095eca..5fbfc08 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/ChannelListFragment.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/ChannelListFragment.kt @@ -12,6 +12,10 @@ import com.eurekateam.fmradio.MainActivity import com.eurekateam.fmradio.R import com.eurekateam.fmradio.adapters.ListViewAdapter 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), View.OnClickListener { @@ -45,11 +49,17 @@ class ChannelListFragment : Fragment(R.layout.activity_channel_list), } override fun onClick(v: View?) { - MainFragment.mRefreshTracks() - (requireActivity() as MainActivity).getMySupportFragmentManager().apply { - beginTransaction().remove(this@ChannelListFragment).commit() - executePendingTransactions() - beginTransaction().add(R.id.container_view, this@ChannelListFragment).commit() + GlobalScope.launch { + withContext(Dispatchers.IO){ + MainFragment.mRefreshTracks() + } + withContext(Dispatchers.Main){ + (requireActivity() as MainActivity).getMySupportFragmentManager().apply { + beginTransaction().remove(this@ChannelListFragment).commit() + executePendingTransactions() + beginTransaction().add(R.id.container_view, this@ChannelListFragment).commit() + } + } } } } \ No newline at end of file diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/MainFragment.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/MainFragment.kt index a952c80..f10a256 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/MainFragment.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/fragments/MainFragment.kt @@ -1,25 +1,27 @@ package com.eurekateam.fmradio.fragments +import android.content.Context import android.content.res.Configuration import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.media.AudioManager import android.os.Bundle -import android.view.Display import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.SeekBar import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatSeekBar import androidx.core.content.res.ResourcesCompat 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.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.textview.MaterialTextView import kotlinx.coroutines.Dispatchers @@ -56,7 +58,7 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, R.drawable.ic_star, requireContext().theme)!! mStarFilled = ResourcesCompat.getDrawable(requireContext().resources, 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) mVolumeDown = mRootView.findViewById(R.id.volume_down) mSeekBar = mRootView.findViewById(R.id.volume_seekbar) @@ -97,15 +99,24 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, else setBackgroundColor(resources.getColor(android.R.color.system_accent1_700, requireContext().theme)) } - var mFMInit = false GlobalScope.launch { 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) { - mAudioManager.setParameters(FM_RADIO_OFF) + mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam) withContext(Dispatchers.Main){ mUpdateEnableDisable(false, mRootView) } - mFMInit = true + mMute = true } if (FileUtilities.checkIfExistFile(FileUtilities.mFMFreqFileName, requireContext())){ @@ -141,27 +152,8 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, } } } - withContext(Dispatchers.IO){ - if (FileUtilities.checkIfExistFile(FileUtilities.mHeadsetFileName, requireContext())){ - 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()) - } - } - } - } + if (!mMute) + mFMInterface.setFMMute(fd, true) mFMInterface.setFMFreq(fd, mFMInterface.getFMLower(fd)) mRefreshTracks() if (mFreqCurrent != -1){ @@ -170,14 +162,13 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, mFreqCurrent = mFMInterface.getFMLower(fd) } mFMInterface.setFMBoot(fd) - if (!mFMInit && isAdded) { - mFreqCurrent = mFMInterface.getFMFreq(fd).toInt() - mFMInterface.setFMFreq(fd, mFreqCurrent) - requireActivity().mainExecutor.execute { - mFMFreq.text = mCleanFormat.format( mFreqCurrent.toFloat() / 1000) - } + mFreqCurrent = mFMInterface.getFMFreq(fd).toInt() + mFMInterface.setFMFreq(fd, mFreqCurrent) + withContext(Dispatchers.Main) { + mFMFreq.text = mCleanFormat.format( mFreqCurrent.toFloat() / 1000) } - if (mTracks.isEmpty()) + if (!mMute) + mFMInterface.setFMMute(fd, false) if (mFreqCurrent == -1) mFMInterface.setFMThread(fd, true) withContext(Dispatchers.Main){ @@ -196,14 +187,34 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, } 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) { alpha = .7f 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) { alpha = 1.0f 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()){ val mTextViewList = listOf(R.id.fm_freq, R.id.freq_misc) val mFloatButtonList = listOf( @@ -227,29 +238,30 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, override fun onClick(v: View) { when (v.id) { mOutputSwitch.id -> { - mAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION - mAudioManager.isSpeakerphoneOn = mHeadset - mAudioManager.setParameters(FM_RADIO_OFF) - mAudioManager.setParameters(FM_RADIO_ON) - mAudioManager.mode = AudioManager.MODE_NORMAL - if (mHeadset) { + if (mHeadset == OutputState.OUTPUT_HEADSET) { mOutputSwitch.setImageIcon( Icon.createWithResource( requireContext(), R.drawable.ic_volume_up ) ) - FileUtilities.createFile(FileUtilities.mHeadsetFileName, requireContext()) - }else{ + val ret = mFMInterface.setAudioRoute(true); + Log.i("mFMInterface.setAudioRoute return $ret"); + }else if (mHeadset == OutputState.OUTPUT_SPEAKER){ mOutputSwitch.setImageIcon( Icon.createWithResource( requireContext(), 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 -> { if (mVolume < 15) @@ -302,10 +314,10 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, } mPowerBtn.id -> { if (mFMPower){ - mAudioManager.setParameters(FM_RADIO_OFF) + mAudioManager.setParameters(PowerState.FM_POWER_OFF.mAudioParam) mFMFreq.text = getText(R.string.inital_freq) }else{ - mAudioManager.setParameters(FM_RADIO_ON) + mAudioManager.setParameters(PowerState.FM_POWER_ON.mAudioParam) mFMFreq.text = mCleanFormat.format(mFreqCurrent.toFloat() / 1000) } mUpdateEnableDisable(!mFMPower) @@ -372,13 +384,11 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, companion object { private var mVolume = -1 var fd = -1 - private var mHeadset = false + var mHeadset = OutputState.OUTPUT_SPEAKER var mFreqCurrent = -1 private var mFMPower = false - var mHeadSetPlugged = false + var mHeadSetPlugged : HeadsetState = HeadsetState.HEADSET_STATE_DISCONNECTED var mTracks : LongArray = emptyArray().toLongArray() - private const val FM_RADIO_ON = "l_fmradio_mode=on" - private const val FM_RADIO_OFF = "l_fmradio_mode=off" fun mRefreshTracks(){ NativeFMInterface().setFMFreq(fd, NativeFMInterface().getFMLower(fd)) mTracks = NativeFMInterface().getFMTracks(fd) @@ -391,9 +401,18 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, val mFavStats = HashMap(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{ val mArray : MutableList = emptyList().toMutableList() - Log.d("removeZeros: Got array") + Log.d("removeZeros: Got array size ${array.size}") for (i in array.indices){ if(array[i] > 0L){ mArray.add(array[i]) @@ -402,4 +421,4 @@ class MainFragment : Fragment(R.layout.fragment_main), View.OnClickListener, return mArray.toLongArray() } -} \ No newline at end of file +} diff --git a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/utils/FileUtilities.kt b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/utils/FileUtilities.kt index 03ad842..e5b8bff 100644 --- a/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/utils/FileUtilities.kt +++ b/universal7885-common/apps/FMRadio/src/com/eurekateam/fmradio/utils/FileUtilities.kt @@ -6,7 +6,7 @@ import java.io.* object FileUtilities { const val mFMFreqFileName = "fm_freq_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){ var os: OutputStream? = null @@ -41,12 +41,7 @@ object FileUtilities { } 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 { return File(mContext.filesDir.absolutePath + "/" + fileName).exists() } diff --git a/universal7885-common/apps/SamsungParts/Android.bp b/universal7885-common/apps/SamsungParts/Android.bp index 3cf02ed..22b97a4 100644 --- a/universal7885-common/apps/SamsungParts/Android.bp +++ b/universal7885-common/apps/SamsungParts/Android.bp @@ -4,7 +4,6 @@ android_app { "src/**/*.kt", ], platform_apis: true, - privileged: true, certificate: "platform", jni_libs: ["libsamsungparts_jni"], static_libs: ["androidx.preference_preference"], diff --git a/universal7885-common/sepolicy/private/file_contexts b/universal7885-common/sepolicy/private/file_contexts index 1ec7550..d0618ad 100644 --- a/universal7885-common/sepolicy/private/file_contexts +++ b/universal7885-common/sepolicy/private/file_contexts @@ -2,4 +2,3 @@ ### DATA /data/zram(/.*)? u:object_r:zram_data_file:s0 -