diff options
author | Benson Huang <benson.huang@mediatek.com> | 2014-11-20 15:42:26 +0800 |
---|---|---|
committer | Nicholas Sauer <nicksauer@google.com> | 2014-12-02 02:44:30 +0000 |
commit | a8b6afca0e187c008ba8fdeb670d5f2c13116bed (patch) | |
tree | fbd1cb3be95858ac34f8f2c262717e38b2b696c6 /src/com/android/fmradio/FmService.java | |
parent | b4f696d7ce27d6ea056120f97fc36967c7178976 (diff) | |
download | android_packages_apps_FMRadio-a8b6afca0e187c008ba8fdeb670d5f2c13116bed.tar.gz android_packages_apps_FMRadio-a8b6afca0e187c008ba8fdeb670d5f2c13116bed.tar.bz2 android_packages_apps_FMRadio-a8b6afca0e187c008ba8fdeb670d5f2c13116bed.zip |
[FM] Move FM Radio sources - Part 4
Move FM Radio sources
From: vendor/mediatek/proprietary/packages/apps/FmRadio/
To: packages/apps/FMRadio
Bug 18057506
https://partner-android-review.googlesource.com/#/c/187247/
Change-Id: Ia6b8b2dfac58a55cffa8d5223ca2ca5a8ca1f9b1
Signed-off-by: Benson Huang <benson.huang@mediatek.com>
Diffstat (limited to 'src/com/android/fmradio/FmService.java')
-rw-r--r-- | src/com/android/fmradio/FmService.java | 2738 |
1 files changed, 2738 insertions, 0 deletions
diff --git a/src/com/android/fmradio/FmService.java b/src/com/android/fmradio/FmService.java new file mode 100644 index 0000000..12fafc9 --- /dev/null +++ b/src/com/android/fmradio/FmService.java @@ -0,0 +1,2738 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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. + */ + +package com.android.fmradio; + +import android.app.ActivityManager; +import android.app.Notification; +import android.app.Notification.BigTextStyle; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.media.AudioDevicePort; +import android.media.AudioDevicePortConfig; +import android.media.AudioFormat; +import android.media.AudioGain; +import android.media.AudioGainConfig; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.media.AudioManager.OnAudioPortUpdateListener; +import android.media.AudioMixPort; +import android.media.AudioPatch; +import android.media.AudioPort; +import android.media.AudioPortConfig; +import android.media.AudioRecord; +import android.media.AudioSystem; +import android.media.AudioTrack; +import android.media.MediaRecorder; +import android.media.VolumeProvider; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.ResultReceiver; +import android.text.TextUtils; +import android.util.Log; + +import com.android.fmradio.FmStation.Station; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Background service to control FM or do background tasks. + */ +public class FmService extends Service implements FmRecorder.OnRecorderStateChangedListener { + // Logging + private static final String TAG = "FmService"; + + // Broadcast messages from other sounder APP to FM service + private static final String SOUND_POWER_DOWN_MSG = "com.android.music.musicservicecommand"; + private static final String FM_SEEK_PREVIOUS = "com.medaitek.fmradio.seek.previous"; + private static final String FM_SEEK_NEXT = "com.medaitek.fmradio.seek.next"; + private static final String FM_TURN_OFF = "com.medaitek.fmradio.turnoff"; + private static final String CMDPAUSE = "pause"; + + // HandlerThread Keys + private static final String FM_FREQUENCY = "frequency"; + private static final String OPTION = "option"; + private static final String RECODING_FILE_NAME = "name"; + + // RDS events + // PS + private static final int RDS_EVENT_PROGRAMNAME = 0x0008; + // RT + private static final int RDS_EVENT_LAST_RADIOTEXT = 0x0040; + // AF + private static final int RDS_EVENT_AF = 0x0080; + + // Headset + private static final int HEADSET_PLUG_IN = 1; + + // Notification id + private static final int NOTIFICATION_ID = 1; + + // Set audio policy for FM + // should check AUDIO_POLICY_FORCE_FOR_MEDIA in audio_policy.h + private static final int FOR_PROPRIETARY = 1; + // Forced Use value + private int mForcedUseForMedia; + + // FM recorder + FmRecorder mFmRecorder = null; + private BroadcastReceiver mSdcardListener = null; + private int mRecordState = FmRecorder.STATE_INVALID; + private int mRecorderErrorType = -1; + // If eject record sdcard, should set Value false to not record. + // Key is sdcard path(like "/storage/sdcard0"), V is to enable record or + // not. + private HashMap<String, Boolean> mSdcardStateMap = new HashMap<String, Boolean>(); + // The show name in save dialog but saved in service + // If modify the save title it will be not null, otherwise it will be null + private String mModifiedRecordingName = null; + // record the listener list, will notify all listener in list + private ArrayList<Record> mRecords = new ArrayList<Record>(); + // record FM whether in recording mode + private boolean mIsInRecordingMode = false; + // record sd card path when start recording + private static String sRecordingSdcard = FmUtils.getDefaultStoragePath(); + + // RDS + // PS String + private String mPsString = ""; + // RT String + private String mRtTextString = ""; + // Notification target class name + private String mTargetClassName = FmMainActivity.class.getName(); + // RDS thread use to receive the information send by station + private Thread mRdsThread = null; + // record whether RDS thread exit + private boolean mIsRdsThreadExit = false; + + // State variables + // Record whether FM is in native scan state + private boolean mIsNativeScanning = false; + // Record whether FM is in scan thread + private boolean mIsScanning = false; + // Record whether FM is in seeking state + private boolean mIsNativeSeeking = false; + // Record whether FM is in native seek + private boolean mIsSeeking = false; + // Record whether searching progress is canceled + private boolean mIsStopScanCalled = false; + // Record whether is speaker used + private boolean mIsSpeakerUsed = false; + // Record whether device is open + private boolean mIsDeviceOpen = false; + // Record Power Status + private int mPowerStatus = POWER_DOWN; + + public static int POWER_UP = 0; + public static int DURING_POWER_UP = 1; + public static int POWER_DOWN = 2; + // Record whether service is init + private boolean mIsServiceInited = false; + // Fm power down by loss audio focus,should make power down menu item can + // click + private boolean mIsPowerDown = false; + // distance is over 100 miles(160934.4m) + private boolean mIsDistanceExceed = false; + // FmMainActivity foreground + private boolean mIsFmMainForeground = true; + // FmFavoriteActivity foreground + private boolean mIsFmFavoriteForground = false; + // Instance variables + private Context mContext = null; + private AudioManager mAudioManager = null; + private ActivityManager mActivityManager = null; + //private MediaPlayer mFmPlayer = null; + private WakeLock mWakeLock = null; + // Audio focus is held or not + private boolean mIsAudioFocusHeld = false; + // Focus transient lost + private boolean mPausedByTransientLossOfFocus = false; + private int mCurrentStation = FmUtils.DEFAULT_STATION; + // Headset plug state (0:long antenna plug in, 1:long antenna plug out) + private int mValueHeadSetPlug = 1; + // For bind service + private final IBinder mBinder = new ServiceBinder(); + // Broadcast to receive the external event + private FmServiceBroadcastReceiver mBroadcastReceiver = null; + // Async handler + private FmRadioServiceHandler mFmServiceHandler; + // Lock for lose audio focus and receive SOUND_POWER_DOWN_MSG + // at the same time + // while recording call stop recording not finished(status is still + // RECORDING), but + // SOUND_POWER_DOWN_MSG will exitFm(), if it is RECORDING will discard the + // record. + // 1. lose audio focus -> stop recording(lock) -> set to IDLE and show save + // dialog + // 2. exitFm() -> check the record status, discard it if it is recording + // status(lock) + // Add this lock the exitFm() while stopRecording() + private Object mStopRecordingLock = new Object(); + // The listener for exit, should finish favorite when exit FM + private static OnExitListener sExitListener = null; + // The latest status for mute/unmute + private boolean mIsMuted = false; + + // Audio Patch + private AudioPatch mAudioPatch = null; + private Object mRenderLock = new Object(); + + private Notification.Builder mNotificationBuilder = null; + private BigTextStyle mNotificationStyle = null; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + /** + * class use to return service instance + */ + public class ServiceBinder extends Binder { + /** + * get FM service instance + * + * @return service instance + */ + FmService getService() { + return FmService.this; + } + } + + /** + * Broadcast monitor external event, Other app want FM stop, Phone shut + * down, screen state, headset state + */ + private class FmServiceBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String command = intent.getStringExtra("command"); + Log.d(TAG, "onReceive, action = " + action + " / command = " + command); + // other app want FM stop, stop FM + if ((SOUND_POWER_DOWN_MSG.equals(action) && CMDPAUSE.equals(command))) { + // need remove all messages, make power down will be execute + mFmServiceHandler.removeCallbacksAndMessages(null); + exitFm(); + stopSelf(); + // phone shut down, so exit FM + } else if (Intent.ACTION_SHUTDOWN.equals(action)) { + /** + * here exitFm, system will send broadcast, system will shut + * down, so fm does not need call back to activity + */ + mFmServiceHandler.removeCallbacksAndMessages(null); + exitFm(); + // screen on, if FM play, open rds + } else if (Intent.ACTION_SCREEN_ON.equals(action)) { + setRdsAsync(true); + // screen off, if FM play, close rds + } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { + setRdsAsync(false); + // switch antenna when headset plug in or plug out + } else if (Intent.ACTION_HEADSET_PLUG.equals(action)) { + // switch antenna should not impact audio focus status + mValueHeadSetPlug = (intent.getIntExtra("state", -1) == HEADSET_PLUG_IN) ? 0 : 1; + switchAntennaAsync(mValueHeadSetPlug); + + // Avoid Service is killed,and receive headset plug in + // broadcast again + if (!mIsServiceInited) { + Log.d(TAG, "onReceive, mIsServiceInited is false"); + return; + } + /* + * If ear phone insert and activity is + * foreground. power up FM automatic + */ + if ((0 == mValueHeadSetPlug) && isActivityForeground()) { + powerUpAsync(FmUtils.computeFrequency(mCurrentStation)); + } else if (1 == mValueHeadSetPlug) { + mFmServiceHandler.removeMessages(FmListener.MSGID_SCAN_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_SEEK_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_TUNE_FINISHED); + mFmServiceHandler.removeMessages( + FmListener.MSGID_POWERDOWN_FINISHED); + mFmServiceHandler.removeMessages( + FmListener.MSGID_POWERUP_FINISHED); + focusChanged(AudioManager.AUDIOFOCUS_LOSS); + + // Need check to switch to earphone mode for audio will + // change to AudioSystem.FORCE_NONE + setForceUse(false); + + // Notify UI change to earphone mode, false means not speaker mode + Bundle bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.LISTEN_SPEAKER_MODE_CHANGED); + bundle.putBoolean(FmListener.KEY_IS_SPEAKER_MODE, false); + notifyActivityStateChanged(bundle); + } + } + } + } + + /** + * Handle sdcard mount/unmount event. 1. Update the sdcard state map 2. If + * the recording sdcard is unmounted, need to stop and notify + */ + private class SdcardListener extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // If eject record sdcard, should set this false to not + // record. + updateSdcardStateMap(intent); + + if (mFmRecorder == null) { + Log.w(TAG, "SdcardListener.onReceive, mFmRecorder is null"); + return; + } + + String action = intent.getAction(); + if (Intent.ACTION_MEDIA_EJECT.equals(action) || + Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) { + // If not unmount recording sd card, do nothing; + if (isRecordingCardUnmount(intent)) { + if (mFmRecorder.getState() == FmRecorder.STATE_RECORDING) { + onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT); + mFmRecorder.discardRecording(); + } else { + Bundle bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.LISTEN_RECORDSTATE_CHANGED); + bundle.putInt(FmListener.KEY_RECORDING_STATE, + FmRecorder.STATE_IDLE); + notifyActivityStateChanged(bundle); + } + } + return; + } + } + } + + /** + * whether antenna available + * + * @return true, antenna available; false, antenna not available + */ + public boolean isAntennaAvailable() { + return mAudioManager.isWiredHeadsetOn(); + } + + private void setForceUse(boolean isSpeaker) { + mForcedUseForMedia = isSpeaker ? AudioSystem.FORCE_SPEAKER : AudioSystem.FORCE_NONE; + AudioSystem.setForceUse(FOR_PROPRIETARY, mForcedUseForMedia); + mIsSpeakerUsed = isSpeaker; + } + + /** + * Set FM audio from speaker or not + * + * @param isSpeaker true if set FM audio from speaker + */ + public void setSpeakerPhoneOn(boolean isSpeaker) { + // TODO it's on UI thread, change to sub thread + if (isSpeaker) { + releaseAudioPatch(); + } + setForceUse(isSpeaker); + + if (isSpeaker) { + startRender(); + } else { + enableFmAudio(true); + } + } + + private synchronized void startRender() { + mIsRender = true; + synchronized (mRenderLock) { + mRenderLock.notify(); + } + } + + private synchronized void stopRender() { + mIsRender = false; + } + + private synchronized void createRenderThread() { + if (mRenderThread == null) { + mRenderThread = new RenderThread(); + mRenderThread.start(); + } + } + + private synchronized void exitRenderThread() { + stopRender(); + mRenderThread.interrupt(); + mRenderThread = null; + } + + private Thread mRenderThread = null; + private AudioRecord mAudioRecord = null; + private AudioTrack mAudioTrack = null; + private static final int SAMPLE_RATE = 44100; + private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_CONFIGURATION_STEREO; + private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; + private static final int RECORD_BUF_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE, + CHANNEL_CONFIG, AUDIO_FORMAT); + private boolean mIsRender = false; + + AudioDevicePort mAudioSource = null; + AudioDevicePort mAudioSink = null; + private int mPatchVolume = 0; + private int mPatchVolumeStep = 300; + + private boolean isRendering() { + return mIsRender; + } + + private void startAudioTrack() { + if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) { + ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); + mAudioManager.listAudioPatches(patches); + Log.e(TAG, "startAudioTrack, patches count:" + patches.size()); + mAudioTrack.play(); + } + } + + private void stopAudioTrack() { + if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { + mAudioTrack.stop(); + } + } + + class RenderThread extends Thread { + @Override + public void run() { + try { + byte[] buffer = new byte[RECORD_BUF_SIZE]; + while (!Thread.interrupted()) { + boolean render = isRender(); + if (render) { + // need rendering + if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) { + mAudioTrack.play(); + } + if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) { + mAudioRecord.startRecording(); + } + int size = mAudioRecord.read(buffer, 0, RECORD_BUF_SIZE); + byte[] tmpBuf = new byte[size]; + System.arraycopy(buffer, 0, tmpBuf, 0, size); + // write to audio track + mAudioTrack.write(tmpBuf, 0, tmpBuf.length); + } else { + // only status wait for render + // stop all + if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { + mAudioRecord.stop(); + } + + // Do not stop audio track to keep the native audio patch + if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { + mAudioTrack.stop(); + } + + //enableFmAudio(true); + synchronized (mRenderLock) { + mRenderLock.wait(); + } + } + } + } catch (InterruptedException e) { + Log.d(TAG, "RenderThread.run, thread is interrupted, need exit thread"); + } finally { + if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { + mAudioRecord.stop(); + } + if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { + mAudioTrack.stop(); + } + } + } + } + + // A2dp or speaker mode should render + private boolean isRender() { + return (mIsRender && (mPowerStatus == POWER_UP) && mIsAudioFocusHeld); + } + + private boolean isSpeakerPhoneOn() { + return (mForcedUseForMedia == AudioSystem.FORCE_SPEAKER); + } + + /** + * open FM device, should be call before power up + * + * @return true if FM device open, false FM device not open + */ + private boolean openDevice() { + if (!mIsDeviceOpen) { + mIsDeviceOpen = FmNative.openDev(); + } + return mIsDeviceOpen; + } + + /** + * close FM device + * + * @return true if close FM device success, false close FM device failed + */ + private boolean closeDevice() { + boolean isDeviceClose = false; + if (mIsDeviceOpen) { + isDeviceClose = FmNative.closeDev(); + mIsDeviceOpen = !isDeviceClose; + } + // quit looper + mFmServiceHandler.getLooper().quit(); + return isDeviceClose; + } + + /** + * get FM device opened or not + * + * @return true FM device opened, false FM device closed + */ + public boolean isDeviceOpen() { + return mIsDeviceOpen; + } + + /** + * power up FM, and make FM voice output from earphone + * + * @param frequency + */ + public void powerUpAsync(float frequency) { + final int bundleSize = 1; + mFmServiceHandler.removeMessages(FmListener.MSGID_POWERUP_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_POWERDOWN_FINISHED); + Bundle bundle = new Bundle(bundleSize); + bundle.putFloat(FM_FREQUENCY, frequency); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_POWERUP_FINISHED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + private boolean powerUp(float frequency) { + if (mPowerStatus == POWER_UP) { + return true; + } + if (!mWakeLock.isHeld()) { + mWakeLock.acquire(); + } + if (!requestAudioFocus()) { + // activity used for update powerdown menu + mPowerStatus = POWER_DOWN; + return false; + } + + mPowerStatus = DURING_POWER_UP; + + // if device open fail when chip reset, it need open device again before + // power up + if (!mIsDeviceOpen) { + openDevice(); + } + + if (!FmNative.powerUp(frequency)) { + mPowerStatus = POWER_DOWN; + return false; + } + mPowerStatus = POWER_UP; + // need mute after power up + setMute(true); + + return (mPowerStatus == POWER_UP); + } + + private boolean playFrequency(float frequency) { + mCurrentStation = FmUtils.computeStation(frequency); + FmStation.setCurrentStation(mContext, mCurrentStation); + // Add notification to the title bar. + updatePlayingNotification(); + + // Start the RDS thread if RDS is supported. + if (isRdsSupported()) { + startRdsThread(); + } + + if (!mWakeLock.isHeld()) { + mWakeLock.acquire(); + } + if (mIsSpeakerUsed != isSpeakerPhoneOn()) { + setForceUse(mIsSpeakerUsed); + } + if (mRecordState != FmRecorder.STATE_PLAYBACK) { + enableFmAudio(true); + } + + setRds(true); + setMute(false); + + return (mPowerStatus == POWER_UP); + } + + /** + * power down FM + */ + public void powerDownAsync() { + // if power down Fm, should remove message first. + // not remove all messages, because such as recorder message need + // to execute after or before power down + mFmServiceHandler.removeMessages(FmListener.MSGID_SCAN_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_SEEK_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_TUNE_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_POWERDOWN_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_POWERUP_FINISHED); + mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_POWERDOWN_FINISHED); + } + + /** + * Power down FM + * + * @return true if power down success + */ + private boolean powerDown() { + if (mPowerStatus == POWER_DOWN) { + return true; + } + + setMute(true); + setRds(false); + enableFmAudio(false); + + if (!FmNative.powerDown(0)) { + + if (isRdsSupported()) { + stopRdsThread(); + } + + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + // Remove the notification in the title bar. + removeNotification(); + return false; + } + // activity used for update powerdown menu + mPowerStatus = POWER_DOWN; + + if (isRdsSupported()) { + stopRdsThread(); + } + + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + + // Remove the notification in the title bar. + removeNotification(); + return true; + } + + public int getPowerStatus() { + return mPowerStatus; + } + + /** + * Tune to a station + * + * @param frequency The frequency to tune + * + * @return true, success; false, fail. + */ + public void tuneStationAsync(float frequency) { + mFmServiceHandler.removeMessages(FmListener.MSGID_TUNE_FINISHED); + final int bundleSize = 1; + Bundle bundle = new Bundle(bundleSize); + bundle.putFloat(FM_FREQUENCY, frequency); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_TUNE_FINISHED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + private boolean tuneStation(float frequency) { + if (mPowerStatus == POWER_UP) { + setRds(false); + boolean bRet = FmNative.tune(frequency); + if (bRet) { + setRds(true); + mCurrentStation = FmUtils.computeStation(frequency); + FmStation.setCurrentStation(mContext, mCurrentStation); + updatePlayingNotification(); + } + setMute(false); + return bRet; + } + + // if earphone is not insert, not power up + if (!isAntennaAvailable()) { + return false; + } + + // if not power up yet, should powerup first + boolean tune = false; + + if (powerUp(frequency)) { + tune = playFrequency(frequency); + } + + return tune; + } + + /** + * Seek station according frequency and direction + * + * @param frequency start frequency(100KHZ, 87.5) + * @param isUp direction(true, next station; false, previous station) + * + * @return the frequency after seek + */ + public void seekStationAsync(float frequency, boolean isUp) { + mFmServiceHandler.removeMessages(FmListener.MSGID_SEEK_FINISHED); + final int bundleSize = 2; + Bundle bundle = new Bundle(bundleSize); + bundle.putFloat(FM_FREQUENCY, frequency); + bundle.putBoolean(OPTION, isUp); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_SEEK_FINISHED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + private float seekStation(float frequency, boolean isUp) { + if (mPowerStatus != POWER_UP) { + return -1; + } + + setRds(false); + mIsNativeSeeking = true; + float fRet = FmNative.seek(frequency, isUp); + mIsNativeSeeking = false; + // make mIsStopScanCalled false, avoid stop scan make this true, + // when start scan, it will return null. + mIsStopScanCalled = false; + return fRet; + } + + /** + * Scan stations + */ + public void startScanAsync() { + mFmServiceHandler.removeMessages(FmListener.MSGID_SCAN_FINISHED); + mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_SCAN_FINISHED); + } + + private int[] startScan() { + int[] stations = null; + + setRds(false); + setMute(true); + short[] stationsInShort = null; + if (!mIsStopScanCalled) { + mIsNativeScanning = true; + stationsInShort = FmNative.autoScan(); + mIsNativeScanning = false; + } + + setRds(true); + if (mIsStopScanCalled) { + // Received a message to power down FM, or interrupted by a phone + // call. Do not return any stations. stationsInShort = null; + // if cancel scan, return invalid station -100 + stationsInShort = new short[] { + -100 + }; + mIsStopScanCalled = false; + } + + if (null != stationsInShort) { + int size = stationsInShort.length; + stations = new int[size]; + for (int i = 0; i < size; i++) { + stations[i] = stationsInShort[i]; + } + } + return stations; + } + + /** + * Check FM Radio is in scan progress or not + * + * @return if in scan progress return true, otherwise return false. + */ + public boolean isScanning() { + return mIsScanning; + } + + /** + * Stop scan progress + * + * @return true if can stop scan, otherwise return false. + */ + public boolean stopScan() { + if (mPowerStatus != POWER_UP) { + return false; + } + + boolean bRet = false; + mFmServiceHandler.removeMessages(FmListener.MSGID_SCAN_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_SEEK_FINISHED); + if (mIsNativeScanning || mIsNativeSeeking) { + mIsStopScanCalled = true; + bRet = FmNative.stopScan(); + } + return bRet; + } + + /** + * Check FM is in seek progress or not + * + * @return true if in seek progress, otherwise return false. + */ + public boolean isSeeking() { + return mIsNativeSeeking; + } + + /** + * Set RDS + * + * @param on true, enable RDS; false, disable RDS. + */ + public void setRdsAsync(boolean on) { + final int bundleSize = 1; + mFmServiceHandler.removeMessages(FmListener.MSGID_SET_RDS_FINISHED); + Bundle bundle = new Bundle(bundleSize); + bundle.putBoolean(OPTION, on); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_SET_RDS_FINISHED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + private int setRds(boolean on) { + if (mPowerStatus != POWER_UP) { + return -1; + } + int ret = -1; + if (isRdsSupported()) { + ret = FmNative.setRds(on); + } + setPs(""); + setLRText(""); + return ret; + } + + /** + * Get PS information + * + * @return PS information + */ + public String getPs() { + return mPsString; + } + + /** + * Get RT information + * + * @return RT information + */ + public String getRtText() { + return mRtTextString; + } + + /** + * Get AF frequency + * + * @return AF frequency + */ + public void activeAfAsync() { + mFmServiceHandler.removeMessages(FmListener.MSGID_ACTIVE_AF_FINISHED); + mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_ACTIVE_AF_FINISHED); + } + + private int activeAf() { + if (mPowerStatus != POWER_UP) { + Log.w(TAG, "activeAf, FM is not powered up"); + return -1; + } + + int frequency = FmNative.activeAf(); + return frequency; + } + + /** + * Mute or unmute FM voice + * + * @param mute true for mute, false for unmute + * + * @return (true, success; false, failed) + */ + public void setMuteAsync(boolean mute) { + mFmServiceHandler.removeMessages(FmListener.MSGID_SET_MUTE_FINISHED); + final int bundleSize = 1; + Bundle bundle = new Bundle(bundleSize); + bundle.putBoolean(OPTION, mute); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_SET_MUTE_FINISHED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + /** + * Mute or unmute FM voice + * + * @param mute true for mute, false for unmute + * + * @return (1, success; other, failed) + */ + public int setMute(boolean mute) { + if (mPowerStatus != POWER_UP) { + Log.w(TAG, "setMute, FM is not powered up"); + return -1; + } + int iRet = FmNative.setMute(mute); + mIsMuted = mute; + return iRet; + } + + /** + * Check the latest status is mute or not + * + * @return (true, mute; false, unmute) + */ + public boolean isMuted() { + return mIsMuted; + } + + /** + * Check whether RDS is support in driver + * + * @return (true, support; false, not support) + */ + public boolean isRdsSupported() { + boolean isRdsSupported = (FmNative.isRdsSupport() == 1); + return isRdsSupported; + } + + /** + * Check whether speaker used or not + * + * @return true if use speaker, otherwise return false + */ + public boolean isSpeakerUsed() { + return mIsSpeakerUsed; + } + + /** + * Initial service and current station + * + * @param iCurrentStation current station frequency + */ + public void initService(int iCurrentStation) { + mIsServiceInited = true; + mCurrentStation = iCurrentStation; + } + + /** + * Check service is initialed or not + * + * @return true if initialed, otherwise return false + */ + public boolean isServiceInited() { + return mIsServiceInited; + } + + /** + * Get FM service current station frequency + * + * @return Current station frequency + */ + public int getFrequency() { + return mCurrentStation; + } + + /** + * Set FM service station frequency + * + * @param station Current station + */ + public void setFrequency(int station) { + mCurrentStation = station; + } + + /** + * resume FM audio + */ + private void resumeFmAudio() { + // If not check mIsAudioFocusHeld && power up, when scan canceled, + // this will be resume first, then execute power down. it will cause + // nosise. + if (mIsAudioFocusHeld && (mPowerStatus == POWER_UP)) { + enableFmAudio(true); + } + } + + /** + * Switch antenna There are two types of antenna(long and short) If long + * antenna(most is this type), must plug in earphone as antenna to receive + * FM. If short antenna, means there is a short antenna if phone already, + * can receive FM without earphone. + * + * @param antenna antenna (0, long antenna, 1 short antenna) + * + * @return (0, success; 1 failed; 2 not support) + */ + public void switchAntennaAsync(int antenna) { + final int bundleSize = 1; + mFmServiceHandler.removeMessages(FmListener.MSGID_SWITCH_ANNTENNA); + + Bundle bundle = new Bundle(bundleSize); + bundle.putInt(FmListener.SWITCH_ANNTENNA_VALUE, antenna); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_SWITCH_ANNTENNA); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + /** + * Need native support whether antenna support interface. + * + * @param antenna antenna (0, long antenna, 1 short antenna) + * + * @return (0, success; 1 failed; 2 not support) + */ + private int switchAntenna(int antenna) { + // if fm not powerup, switchAntenna will flag whether has earphone + int ret = FmNative.switchAntenna(antenna); + return ret; + } + + /** + * Start recording + */ + public void startRecordingAsync() { + mFmServiceHandler.removeMessages(FmListener.MSGID_STARTRECORDING_FINISHED); + mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_STARTRECORDING_FINISHED); + } + + private void startRecording() { + sRecordingSdcard = FmUtils.getDefaultStoragePath(); + if (sRecordingSdcard == null || sRecordingSdcard.isEmpty()) { + Log.d(TAG, "startRecording, may be no sdcard"); + onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT); + return; + } + + if (mFmRecorder == null) { + mFmRecorder = new FmRecorder(); + mFmRecorder.registerRecorderStateListener(FmService.this); + } + + if (isSdcardReady(sRecordingSdcard)) { + mFmRecorder.startRecording(mContext); + } else { + onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT); + } + } + + private boolean isSdcardReady(String sdcardPath) { + if (!mSdcardStateMap.isEmpty()) { + if (mSdcardStateMap.get(sdcardPath) != null && !mSdcardStateMap.get(sdcardPath)) { + Log.d(TAG, "isSdcardReady, return false"); + return false; + } + } + return true; + } + + /** + * stop recording + */ + public void stopRecordingAsync() { + mFmServiceHandler.removeMessages(FmListener.MSGID_STOPRECORDING_FINISHED); + mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_STOPRECORDING_FINISHED); + } + + private boolean stopRecording() { + if (mFmRecorder == null) { + Log.e(TAG, "stopRecording, called without a valid recorder!!"); + return false; + } + synchronized (mStopRecordingLock) { + mFmRecorder.stopRecording(); + } + return true; + } + + /** + * Save recording file according name or discard recording file if name is + * null + * + * @param newName New recording file name + */ + public void saveRecordingAsync(String newName) { + mFmServiceHandler.removeMessages(FmListener.MSGID_SAVERECORDING_FINISHED); + final int bundleSize = 1; + Bundle bundle = new Bundle(bundleSize); + bundle.putString(RECODING_FILE_NAME, newName); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_SAVERECORDING_FINISHED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + private void saveRecording(String newName) { + if (mFmRecorder != null) { + if (newName != null) { + mFmRecorder.saveRecording(FmService.this, newName); + return; + } + mFmRecorder.discardRecording(); + } + } + + /** + * Get record time + * + * @return Record time + */ + public long getRecordTime() { + if (mFmRecorder != null) { + return mFmRecorder.getRecordTime(); + } + return 0; + } + + /** + * Set recording mode + * + * @param isRecording true, enter recoding mode; false, exit recording mode + */ + public void setRecordingModeAsync(boolean isRecording) { + mFmServiceHandler.removeMessages(FmListener.MSGID_RECORD_MODE_CHANED); + final int bundleSize = 1; + Bundle bundle = new Bundle(bundleSize); + bundle.putBoolean(OPTION, isRecording); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_RECORD_MODE_CHANED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + private void setRecordingMode(boolean isRecording) { + mIsInRecordingMode = isRecording; + if (mFmRecorder != null) { + if (!isRecording) { + if (mFmRecorder.getState() != FmRecorder.STATE_IDLE) { + mFmRecorder.stopRecording(); + } + resumeFmAudio(); + setMute(false); + return; + } + // reset recorder to unused status + mFmRecorder.resetRecorder(); + } + } + + /** + * Get current recording mode + * + * @return if in recording mode return true, otherwise return false; + */ + public boolean getRecordingMode() { + return mIsInRecordingMode; + } + + /** + * Get record state + * + * @return record state + */ + public int getRecorderState() { + if (null != mFmRecorder) { + return mFmRecorder.getState(); + } + return FmRecorder.STATE_INVALID; + } + + /** + * Get recording file name + * + * @return recording file name + */ + public String getRecordingName() { + if (null != mFmRecorder) { + return mFmRecorder.getRecordFileName(); + } + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + mContext = getApplicationContext(); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(false); + sRecordingSdcard = FmUtils.getDefaultStoragePath(); + + registerFmBroadcastReceiver(); + registerSdcardReceiver(); + registerAudioPortUpdateListener(); + + HandlerThread handlerThread = new HandlerThread("FmRadioServiceThread"); + handlerThread.start(); + mFmServiceHandler = new FmRadioServiceHandler(handlerThread.getLooper()); + + openDevice(); + // set speaker to default status, avoid setting->clear data. + setForceUse(mIsSpeakerUsed); + + initAudioRecordSink(); + createRenderThread(); + + int v = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + mPatchVolume = computeGainVolume(v); + } + + private MediaSession mSession; + private PlaybackState mPlaybackState; + private MediaSession.Callback mSessionCallback; + + private void createMediaSession() { + VolumeProvider vp = new VolumeProvider(VolumeProvider.VOLUME_CONTROL_RELATIVE, 15, 7) { + public void onSetVolumeTo(int volume) { + mPatchVolume = computeGainVolume(volume); + adjustAudioGain(mPatchVolume); + } + + public void onAdjustVolume(int direction) { + if (direction == 0) { + return; + } + + int current = getCurrentVolume() + direction; + if (current <= getMaxVolume() && current >= 0) { + setCurrentVolume(current); + mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, 0); + int v = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + if (direction > 0) { + volumeUp(); + } else { + volumeDown(); + } + } + } + }; + + mSessionCallback = new SessionCb(); + PlaybackState.Builder psBob = new PlaybackState.Builder(); + mPlaybackState = psBob.setState(PlaybackState.STATE_PLAYING, 0, 0).build(); + MediaSessionManager man = (MediaSessionManager) mContext + .getSystemService(Context.MEDIA_SESSION_SERVICE); + mSession = new MediaSession(mContext, "OneMedia"); + MediaController controller = mSession.getController(); + mSession.setCallback(mSessionCallback); + mSession.setPlaybackState(mPlaybackState); + mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mSession.setActive(true); + mSession.setPlaybackToRemote(vp); + } + + private class SessionCb extends MediaSession.Callback { + @Override + public void onPlay() { + } + + @Override + public void onPause() { + } + + @Override + public void onCommand(String command, Bundle args, ResultReceiver cb) { + super.onCommand(command, args, cb); + } + + @Override + public boolean onMediaButtonEvent(Intent intent) { + return super.onMediaButtonEvent(intent); + } + } + + private void releaseMediaSession() { + if (mSession != null) { + mSession.release(); + mSession = null; + } + } + + private void registerAudioPortUpdateListener() { + if (mAudioPortUpdateListener == null) { + mAudioPortUpdateListener = new FmOnAudioPortUpdateListener(); + mAudioManager.registerAudioPortUpdateListener(mAudioPortUpdateListener); + } + } + + private void unregisterAudioPortUpdateListener() { + if (mAudioPortUpdateListener != null) { + mAudioManager.unregisterAudioPortUpdateListener(mAudioPortUpdateListener); + mAudioPortUpdateListener = null; + } + } + + private void initAudioRecordSink() { + mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.FM_TUNER, + SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, RECORD_BUF_SIZE); + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, + SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, RECORD_BUF_SIZE, AudioTrack.MODE_STREAM); + } + + private synchronized void createAudioPatch() { + if (mAudioPatch != null) { + Log.d(TAG, "createAudioPatch, mAudioPatch is not null, return"); + return; + } + + mAudioSource = null; + mAudioSink = null; + ArrayList<AudioPort> ports = new ArrayList<AudioPort>(); + mAudioManager.listAudioPorts(ports); + for (AudioPort port : ports) { + if (port instanceof AudioDevicePort) { + int type = ((AudioDevicePort) port).type(); + String name = AudioSystem.getOutputDeviceName(type); + if (type == AudioSystem.DEVICE_IN_FM_TUNER) { + mAudioSource = (AudioDevicePort) port; + } else if (type == AudioSystem.DEVICE_OUT_WIRED_HEADSET || + type == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE) { + mAudioSink = (AudioDevicePort) port; + } + } + } + if (mAudioSource != null && mAudioSink != null) { + AudioDevicePortConfig sourceConfig = (AudioDevicePortConfig) mAudioSource + .activeConfig(); + AudioDevicePortConfig sinkConfig = (AudioDevicePortConfig) mAudioSink.activeConfig(); + AudioPatch[] audioPatchArray = new AudioPatch[] {null}; + mAudioManager.createAudioPatch(audioPatchArray, + new AudioPortConfig[] {sourceConfig}, + new AudioPortConfig[] {sinkConfig}); + mAudioPatch = audioPatchArray[0]; + adjustAudioGain(mPatchVolume); + createMediaSession(); + } + } + + private void adjustAudioGain(int volume) { + if (mAudioPatch == null || mAudioSource == null || mAudioSink == null) { + return; + } + + AudioGainConfig sinkGainConfig = null; + if (mAudioSink.gains().length > 0) { + AudioGain sinkGain = null; + for (AudioGain gain : mAudioSink.gains()) { + if ((gain.mode() & AudioGain.MODE_JOINT) != 0) { + sinkGain = gain; + break; + } + } + + // NOTE: we only change the source gain in MODE_JOINT here. + if (sinkGain != null) { + int numChannels = 0; + for (int mask = sinkGain.channelMask(); mask > 0; mask >>= 1) { + numChannels += (mask & 1); + } + int[] gainValues = new int[numChannels]; + Arrays.fill(gainValues, volume); + sinkGainConfig = sinkGain.buildConfig(AudioGain.MODE_JOINT, + 0x08, gainValues, 0); + } + } + + AudioDevicePortConfig sinkConfig = mAudioSink.buildConfig(SAMPLE_RATE, + CHANNEL_CONFIG, AUDIO_FORMAT, sinkGainConfig); + mAudioManager.setAudioPortGain(mAudioSink, sinkGainConfig); + } + + // 0 ~ 15 + private int computeGainVolume(int streamVolume) { + return 0 - (15 - streamVolume) * mPatchVolumeStep; + } + + private FmOnAudioPortUpdateListener mAudioPortUpdateListener = null; + + private class FmOnAudioPortUpdateListener implements OnAudioPortUpdateListener { + /** + * Callback method called upon audio port list update. + * @param portList the updated list of audio ports + */ + @Override + public void onAudioPortListUpdate(AudioPort[] portList) { + if (mPowerStatus != POWER_UP) { + Log.d(TAG, "onAudioPortListUpdate, not power up, return"); + return; + } + if (!mIsAudioFocusHeld) { + Log.d(TAG, "onAudioPortListUpdate, current not available return." + + "mIsAudioFocusHeld:" + mIsAudioFocusHeld); + return; + } + + if (mAudioPatch != null) { + startAudioTrack(); + ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); + mAudioManager.listAudioPatches(patches); + if (isPatchMixerToEarphone(patches)) { + stopAudioTrack(); + stopRender(); + } else { + releaseAudioPatch(); + startRender(); + } + } + } + + /** + * Callback method called upon audio patch list update. + * + * @param patchList the updated list of audio patches + */ + @Override + public void onAudioPatchListUpdate(AudioPatch[] patchList) { + if (mPowerStatus != POWER_UP) { + Log.d(TAG, "onAudioPortListUpdate, not power up"); + return; + } + + if (!mIsAudioFocusHeld) { + Log.d(TAG, "onAudioPortListUpdate, Current not available return." + + "mIsAudioFocusHeld:" + mIsAudioFocusHeld); + return; + } + + if (mAudioPatch != null) { + startAudioTrack(); + ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); + mAudioManager.listAudioPatches(patches); + if (isPatchMixerToEarphone(patches)) { + stopAudioTrack(); + stopRender(); + } else { + releaseAudioPatch(); + startRender(); + } + } else if (mIsRender) { + ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); + mAudioManager.listAudioPatches(patches); + if (isPatchMixerToEarphone(patches)) { + stopAudioTrack(); + stopRender(); + createAudioPatch(); + } + } + } + + /** + * Callback method called when the mediaserver dies + */ + @Override + public void onServiceDied() { + enableFmAudio(false); + } + } + + private synchronized void releaseAudioPatch() { + if (mAudioPatch != null) { + mAudioManager.releaseAudioPatch(mAudioPatch); + mAudioPatch = null; + releaseMediaSession(); + } + mAudioSource = null; + mAudioSink = null; + } + + private void registerFmBroadcastReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(SOUND_POWER_DOWN_MSG); + filter.addAction(Intent.ACTION_SHUTDOWN); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_HEADSET_PLUG); + mBroadcastReceiver = new FmServiceBroadcastReceiver(); + registerReceiver(mBroadcastReceiver, filter); + } + + private void unregisterFmBroadcastReceiver() { + if (null != mBroadcastReceiver) { + unregisterReceiver(mBroadcastReceiver); + mBroadcastReceiver = null; + } + } + + @Override + public void onDestroy() { + mAudioManager.setParameters("AudioFmPreStop=1"); + setMute(true); + // stop rds first, avoid blocking other native method + if (isRdsSupported()) { + stopRdsThread(); + } + unregisterFmBroadcastReceiver(); + unregisterSdcardListener(); + abandonAudioFocus(); + exitFm(); + if (null != mFmRecorder) { + mFmRecorder = null; + } + exitRenderThread(); + releaseAudioPatch(); + releaseMediaSession(); + unregisterAudioPortUpdateListener(); + super.onDestroy(); + } + + /** + * Exit FMRadio application + */ + private void exitFm() { + mIsAudioFocusHeld = false; + // Stop FM recorder if it is working + if (null != mFmRecorder) { + synchronized (mStopRecordingLock) { + int fmState = mFmRecorder.getState(); + if (FmRecorder.STATE_RECORDING == fmState) { + mFmRecorder.stopRecording(); + } + } + } + + // When exit, we set the audio path back to earphone. + if (mIsNativeScanning || mIsNativeSeeking) { + stopScan(); + } + + mFmServiceHandler.removeCallbacksAndMessages(null); + mFmServiceHandler.removeMessages(FmListener.MSGID_FM_EXIT); + mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_FM_EXIT); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Change the notification string. + if (mPowerStatus == POWER_UP) { + showPlayingNotification(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + int ret = super.onStartCommand(intent, flags, startId); + + if (intent != null) { + String action = intent.getAction(); + if (FM_SEEK_PREVIOUS.equals(action)) { + seekStationAsync(FmUtils.computeFrequency(mCurrentStation), false); + } else if (FM_SEEK_NEXT.equals(action)) { + seekStationAsync(FmUtils.computeFrequency(mCurrentStation), true); + } else if (FM_TURN_OFF.equals(action)) { + powerDownAsync(); + } + } + return START_NOT_STICKY; + } + + /** + * Start RDS thread to update RDS information + */ + private void startRdsThread() { + mIsRdsThreadExit = false; + if (null != mRdsThread) { + return; + } + mRdsThread = new Thread() { + public void run() { + while (true) { + if (mIsRdsThreadExit) { + break; + } + + int iRdsEvents = FmNative.readRds(); + if (iRdsEvents != 0) { + Log.d(TAG, "startRdsThread, is rds events: " + iRdsEvents); + } + + if (RDS_EVENT_PROGRAMNAME == (RDS_EVENT_PROGRAMNAME & iRdsEvents)) { + byte[] bytePS = FmNative.getPs(); + if (null != bytePS) { + String ps = new String(bytePS).trim(); + if (!mPsString.equals(ps)) { + updatePlayingNotification(); + } + ContentValues values = null; + if (FmStation.isStationExist(mContext, mCurrentStation)) { + values = new ContentValues(1); + values.put(Station.PROGRAM_SERVICE, ps); + FmStation.updateStationToDb(mContext, mCurrentStation, values); + } else { + values = new ContentValues(2); + values.put(Station.FREQUENCY, mCurrentStation); + values.put(Station.PROGRAM_SERVICE, ps); + FmStation.insertStationToDb(mContext, values); + } + setPs(ps); + } + } + + if (RDS_EVENT_LAST_RADIOTEXT == (RDS_EVENT_LAST_RADIOTEXT & iRdsEvents)) { + byte[] byteLRText = FmNative.getLrText(); + if (null != byteLRText) { + String rds = new String(byteLRText).trim(); + setLRText(rds); + ContentValues values = null; + if (FmStation.isStationExist(mContext, mCurrentStation)) { + values = new ContentValues(1); + values.put(Station.RADIO_TEXT, rds); + FmStation.updateStationToDb(mContext, mCurrentStation, values); + } else { + values = new ContentValues(2); + values.put(Station.FREQUENCY, mCurrentStation); + values.put(Station.RADIO_TEXT, rds); + FmStation.insertStationToDb(mContext, values); + } + } + } + + if (RDS_EVENT_AF == (RDS_EVENT_AF & iRdsEvents)) { + /* + * add for rds AF + */ + if (mIsScanning || mIsSeeking) { + Log.d(TAG, "startRdsThread, seek or scan going, no need to tune here"); + } else if (mPowerStatus == POWER_DOWN) { + Log.d(TAG, "startRdsThread, fm is power down, do nothing."); + } else { + int iFreq = FmNative.activeAf(); + if (FmUtils.isValidStation(iFreq)) { + // if the new frequency is not equal to current + // frequency. + if (mCurrentStation != iFreq) { + if (!mIsScanning && !mIsSeeking) { + Log.d(TAG, "startRdsThread, seek or scan not going," + + "need to tune here"); + tuneStationAsync(FmUtils.computeFrequency(iFreq)); + } + } + } + } + } + // Do not handle other events. + // Sleep 500ms to reduce inquiry frequency + try { + final int hundredMillisecond = 500; + Thread.sleep(hundredMillisecond); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }; + mRdsThread.start(); + } + + /** + * Stop RDS thread to stop listen station RDS change + */ + private void stopRdsThread() { + if (null != mRdsThread) { + // Must call closedev after stopRDSThread. + mIsRdsThreadExit = true; + mRdsThread = null; + } + } + + /** + * Set PS information + * + * @param ps The ps information + */ + private void setPs(String ps) { + if (0 != mPsString.compareTo(ps)) { + mPsString = ps; + Bundle bundle = new Bundle(3); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_PS_CHANGED); + bundle.putString(FmListener.KEY_PS_INFO, mPsString); + notifyActivityStateChanged(bundle); + } // else New PS is the same as current + } + + /** + * Set RT information + * + * @param lrtText The RT information + */ + private void setLRText(String lrtText) { + if (0 != mRtTextString.compareTo(lrtText)) { + mRtTextString = lrtText; + Bundle bundle = new Bundle(3); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_RT_CHANGED); + bundle.putString(FmListener.KEY_RT_INFO, mRtTextString); + notifyActivityStateChanged(bundle); + } // else New RT is the same as current + } + + /** + * Open or close FM Radio audio + * + * @param enable true, open FM audio; false, close FM audio; + */ + private void enableFmAudio(boolean enable) { + if (enable) { + if ((mPowerStatus != POWER_UP) || !mIsAudioFocusHeld) { + Log.d(TAG, "enableFmAudio, current not available return.mIsAudioFocusHeld:" + + mIsAudioFocusHeld); + return; + } + + startAudioTrack(); + ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); + mAudioManager.listAudioPatches(patches); + if (mAudioPatch == null) { + if (isPatchMixerToEarphone(patches)) { + stopAudioTrack(); + stopRender(); + createAudioPatch(); + } else { + startRender(); + } + } + } else { + releaseAudioPatch(); + stopRender(); + } + } + + // Make sure patches count will not be 0 + private boolean isPatchMixerToEarphone(ArrayList<AudioPatch> patches) { + int deviceCount = 0; + int deviceEarphoneCount = 0; + for (AudioPatch patch : patches) { + AudioPortConfig[] sources = patch.sources(); + AudioPortConfig[] sinks = patch.sinks(); + AudioPortConfig sourceConfig = sources[0]; + AudioPortConfig sinkConfig = sinks[0]; + AudioPort sourcePort = sourceConfig.port(); + AudioPort sinkPort = sinkConfig.port(); + if (sourcePort instanceof AudioMixPort && sinkPort instanceof AudioDevicePort) { + deviceCount++; + int type = ((AudioDevicePort) sinkPort).type(); + if (type == AudioSystem.DEVICE_OUT_WIRED_HEADSET || + type == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE) { + deviceEarphoneCount++; + } + } + } + if (deviceEarphoneCount == 1 && deviceCount == deviceEarphoneCount) { + return true; + } + return false; + } + + /** + * Show notification + */ + private void showPlayingNotification() { + if (isActivityForeground() || mIsScanning + || (getRecorderState() == FmRecorder.STATE_RECORDING)) { + Log.w(TAG, "showPlayingNotification, do not show main notification."); + return; + } + String stationName = ""; + String ratioText = ""; + ContentResolver resolver = mContext.getContentResolver(); + Cursor cursor = null; + try { + cursor = resolver.query( + Station.CONTENT_URI, + FmStation.COLUMNS, + Station.FREQUENCY + "=?", + new String[] { String.valueOf(mCurrentStation) }, + null); + if (cursor != null && cursor.moveToFirst()) { + // If the station name is not exist, show program service(PS) instead + stationName = cursor.getString(cursor.getColumnIndex(Station.STATION_NAME)); + if (TextUtils.isEmpty(stationName)) { + stationName = cursor.getString(cursor.getColumnIndex(Station.PROGRAM_SERVICE)); + } + ratioText = cursor.getString(cursor.getColumnIndex(Station.RADIO_TEXT)); + + } else { + Log.d(TAG, "showPlayingNotification, cursor is null"); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + Intent aIntent = new Intent(Intent.ACTION_MAIN); + aIntent.addCategory(Intent.CATEGORY_LAUNCHER); + aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + aIntent.setClassName(getPackageName(), mTargetClassName); + PendingIntent pAIntent = PendingIntent.getActivity(mContext, 0, aIntent, 0); + + if (null == mNotificationBuilder) { + mNotificationBuilder = new Notification.Builder(mContext); + mNotificationBuilder.setSmallIcon(R.drawable.ic_launcher); + mNotificationBuilder.setShowWhen(false); + mNotificationBuilder.setAutoCancel(true); + + Intent intent = new Intent(FM_SEEK_PREVIOUS); + intent.setClass(mContext, FmService.class); + PendingIntent pIntent = PendingIntent.getService(mContext, 0, intent, 0); + mNotificationBuilder.addAction(R.drawable.btn_fm_prevstation, "", pIntent); + intent = new Intent(FM_TURN_OFF); + intent.setClass(mContext, FmService.class); + pIntent = PendingIntent.getService(mContext, 0, intent, 0); + mNotificationBuilder.addAction(R.drawable.btn_fm_rec_stop_enabled, "", pIntent); + intent = new Intent(FM_SEEK_NEXT); + intent.setClass(mContext, FmService.class); + pIntent = PendingIntent.getService(mContext, 0, intent, 0); + mNotificationBuilder.addAction(R.drawable.btn_fm_nextstation, "", pIntent); + } + mNotificationBuilder.setContentIntent(pAIntent); + Bitmap largeIcon = FmUtils.createNotificationLargeIcon(mContext, + FmUtils.formatStation(mCurrentStation)); + mNotificationBuilder.setLargeIcon(largeIcon); + // Show FM Radio if empty + if (TextUtils.isEmpty(stationName)) { + stationName = getString(R.string.app_name); + } + mNotificationBuilder.setContentTitle(stationName); + if (!TextUtils.isEmpty(ratioText)) { + mNotificationBuilder.setContentText(ratioText); + } + + Notification n = mNotificationBuilder.build(); + n.flags &= ~Notification.FLAG_NO_CLEAR; + startForeground(NOTIFICATION_ID, n); + } + + /** + * Show notification + */ + public void showRecordingNotification(Notification notification) { + startForeground(NOTIFICATION_ID, notification); + } + + /** + * Remove notification + */ + public void removeNotification() { + stopForeground(true); + } + + /** + * Update notification + */ + public void updatePlayingNotification() { + if (mPowerStatus == POWER_UP) { + showPlayingNotification(); + } + } + + /** + * Register sdcard listener for record + */ + private void registerSdcardReceiver() { + if (mSdcardListener == null) { + mSdcardListener = new SdcardListener(); + } + IntentFilter filter = new IntentFilter(); + filter.addDataScheme("file"); + filter.addAction(Intent.ACTION_MEDIA_MOUNTED); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + filter.addAction(Intent.ACTION_MEDIA_EJECT); + registerReceiver(mSdcardListener, filter); + } + + private void unregisterSdcardListener() { + if (null != mSdcardListener) { + unregisterReceiver(mSdcardListener); + } + } + + private void updateSdcardStateMap(Intent intent) { + String action = intent.getAction(); + String sdcardPath = null; + Uri mountPointUri = intent.getData(); + if (mountPointUri != null) { + sdcardPath = mountPointUri.getPath(); + if (sdcardPath != null) { + if (Intent.ACTION_MEDIA_EJECT.equals(action)) { + mSdcardStateMap.put(sdcardPath, false); + } else if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) { + mSdcardStateMap.put(sdcardPath, false); + } else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) { + mSdcardStateMap.put(sdcardPath, true); + } + } + } + } + + /** + * Notify FM recorder state + * + * @param state The current FM recorder state + */ + @Override + public void onRecorderStateChanged(int state) { + mRecordState = state; + Bundle bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_RECORDSTATE_CHANGED); + bundle.putInt(FmListener.KEY_RECORDING_STATE, state); + notifyActivityStateChanged(bundle); + } + + /** + * Notify FM recorder error message + * + * @param error The recorder error type + */ + @Override + public void onRecorderError(int error) { + // if media server die, will not enable FM audio, and convert to + // ERROR_PLAYER_INATERNAL, call back to activity showing toast. + mRecorderErrorType = error; + + Bundle bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_RECORDERROR); + bundle.putInt(FmListener.KEY_RECORDING_ERROR_TYPE, mRecorderErrorType); + notifyActivityStateChanged(bundle); + } + + /** + * Check and go next(play or show tips) after recorder file play + * back finish. + * Two cases: + * 1. With headset -> play FM + * 2. Without headset -> show plug in earphone tips + */ + private void checkState() { + if (isHeadSetIn()) { + // with headset + if (mPowerStatus == POWER_UP) { + resumeFmAudio(); + setMute(false); + } else { + powerUpAsync(FmUtils.computeFrequency(mCurrentStation)); + } + } else { + // without headset need show plug in earphone tips + switchAntennaAsync(mValueHeadSetPlug); + } + } + + /** + * Check the headset is plug in or plug out + * + * @return true for plug in; false for plug out + */ + private boolean isHeadSetIn() { + return (0 == mValueHeadSetPlug); + } + + private void focusChanged(int focusState) { + mIsAudioFocusHeld = false; + if (mIsNativeScanning || mIsNativeSeeking) { + // make stop scan from activity call to service. + // notifyActivityStateChanged(FMRadioListener.LISTEN_SCAN_CANCELED); + stopScan(); + } + + // using handler thread to update audio focus state + updateAudioFocusAync(focusState); + } + + /** + * Request audio focus + * + * @return true, success; false, fail; + */ + public boolean requestAudioFocus() { + if (mIsAudioFocusHeld) { + return true; + } + + int audioFocus = mAudioManager.requestAudioFocus(mAudioFocusChangeListener, + AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + mIsAudioFocusHeld = (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == audioFocus); + return mIsAudioFocusHeld; + } + + /** + * Abandon audio focus + */ + public void abandonAudioFocus() { + mAudioManager.abandonAudioFocus(mAudioFocusChangeListener); + mIsAudioFocusHeld = false; + } + + /** + * Use to interact with other voice related app + */ + private final OnAudioFocusChangeListener mAudioFocusChangeListener = + new OnAudioFocusChangeListener() { + /** + * Handle audio focus change ensure message FIFO + * + * @param focusChange audio focus change state + */ + @Override + public void onAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + synchronized (this) { + mAudioManager.setParameters("AudioFmPreStop=1"); + setMute(true); + focusChanged(AudioManager.AUDIOFOCUS_LOSS); + } + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + synchronized (this) { + mAudioManager.setParameters("AudioFmPreStop=1"); + setMute(true); + focusChanged(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); + } + break; + + case AudioManager.AUDIOFOCUS_GAIN: + synchronized (this) { + updateAudioFocusAync(AudioManager.AUDIOFOCUS_GAIN); + } + break; + + default: + break; + } + } + }; + + /** + * Audio focus changed, will send message to handler thread. synchronized to + * ensure one message can go in this method. + * + * @param focusState AudioManager state + */ + private synchronized void updateAudioFocusAync(int focusState) { + final int bundleSize = 1; + Bundle bundle = new Bundle(bundleSize); + bundle.putInt(FmListener.KEY_AUDIOFOCUS_CHANGED, focusState); + Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_AUDIOFOCUS_CHANGED); + msg.setData(bundle); + mFmServiceHandler.sendMessage(msg); + } + + /** + * Audio focus changed, update FM focus state. + * + * @param focusState AudioManager state + */ + private void updateAudioFocus(int focusState) { + switch (focusState) { + case AudioManager.AUDIOFOCUS_LOSS: + mPausedByTransientLossOfFocus = false; + // play back audio will output with music audio + // May be affect other recorder app, but the flow can not be + // execute earlier, + // It should ensure execute after start/stop record. + if (mFmRecorder != null) { + int fmState = mFmRecorder.getState(); + // only handle recorder state, not handle playback state + if (fmState == FmRecorder.STATE_RECORDING) { + mFmServiceHandler.removeMessages( + FmListener.MSGID_STARTRECORDING_FINISHED); + mFmServiceHandler.removeMessages( + FmListener.MSGID_STOPRECORDING_FINISHED); + stopRecording(); + } + } + handlePowerDown(); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + if (mPowerStatus == POWER_UP) { + mPausedByTransientLossOfFocus = true; + } + // play back audio will output with music audio + // May be affect other recorder app, but the flow can not be + // execute earlier, + // It should ensure execute after start/stop record. + if (mFmRecorder != null) { + int fmState = mFmRecorder.getState(); + if (fmState == FmRecorder.STATE_RECORDING) { + mFmServiceHandler.removeMessages( + FmListener.MSGID_STARTRECORDING_FINISHED); + mFmServiceHandler.removeMessages( + FmListener.MSGID_STOPRECORDING_FINISHED); + stopRecording(); + } + } + handlePowerDown(); + break; + + case AudioManager.AUDIOFOCUS_GAIN: + if ((mPowerStatus != POWER_UP) && mPausedByTransientLossOfFocus) { + final int bundleSize = 1; + mFmServiceHandler.removeMessages(FmListener.MSGID_POWERUP_FINISHED); + mFmServiceHandler.removeMessages(FmListener.MSGID_POWERDOWN_FINISHED); + Bundle bundle = new Bundle(bundleSize); + bundle.putFloat(FM_FREQUENCY, FmUtils.computeFrequency(mCurrentStation)); + handlePowerUp(bundle); + } + break; + + default: + break; + } + } + + /** + * FM Radio listener record + */ + private static class Record { + int mHashCode; // hash code + FmListener mCallback; // call back + } + + /** + * Register FM Radio listener, activity get service state should call this + * method register FM Radio listener + * + * @param callback FM Radio listener + */ + public void registerFmRadioListener(FmListener callback) { + synchronized (mRecords) { + // register callback in AudioProfileService, if the callback is + // exist, just replace the event. + Record record = null; + int hashCode = callback.hashCode(); + final int n = mRecords.size(); + for (int i = 0; i < n; i++) { + record = mRecords.get(i); + if (hashCode == record.mHashCode) { + return; + } + } + record = new Record(); + record.mHashCode = hashCode; + record.mCallback = callback; + mRecords.add(record); + } + } + + /** + * Call back from service to activity + * + * @param bundle The message to activity + */ + private void notifyActivityStateChanged(Bundle bundle) { + if (!mRecords.isEmpty()) { + synchronized (mRecords) { + Iterator<Record> iterator = mRecords.iterator(); + while (iterator.hasNext()) { + Record record = (Record) iterator.next(); + + FmListener listener = record.mCallback; + + if (listener == null) { + iterator.remove(); + return; + } + + listener.onCallBack(bundle); + } + } + } + } + + /** + * Call back from service to the current request activity + * Scan need only notify FmFavoriteActivity if current is FmFavoriteActivity + * + * @param bundle The message to activity + */ + private void notifyCurrentActivityStateChanged(Bundle bundle) { + if (!mRecords.isEmpty()) { + Log.d(TAG, "notifyCurrentActivityStateChanged = " + mRecords.size()); + synchronized (mRecords) { + if (mRecords.size() > 0) { + Record record = mRecords.get(mRecords.size() - 1); + FmListener listener = record.mCallback; + if (listener == null) { + mRecords.remove(record); + return; + } + listener.onCallBack(bundle); + } + } + } + } + + /** + * Unregister FM Radio listener + * + * @param callback FM Radio listener + */ + public void unregisterFmRadioListener(FmListener callback) { + remove(callback.hashCode()); + } + + /** + * Remove call back according hash code + * + * @param hashCode The call back hash code + */ + private void remove(int hashCode) { + synchronized (mRecords) { + Iterator<Record> iterator = mRecords.iterator(); + while (iterator.hasNext()) { + Record record = (Record) iterator.next(); + if (record.mHashCode == hashCode) { + iterator.remove(); + } + } + } + } + + /** + * Check recording sd card is unmount + * + * @param intent The unmount sd card intent + * + * @return true or false indicate whether current recording sd card is + * unmount or not + */ + public boolean isRecordingCardUnmount(Intent intent) { + String unmountSDCard = intent.getData().toString(); + Log.d(TAG, "unmount sd card file path: " + unmountSDCard); + return unmountSDCard.equalsIgnoreCase("file://" + sRecordingSdcard) ? true : false; + } + + private int[] updateStations(int[] stations) { + Log.d(TAG, "updateStations.firstValidstation:" + Arrays.toString(stations)); + int firstValidstation = mCurrentStation; + + int stationNum = 0; + if (null != stations) { + int searchedListSize = stations.length; + if (mIsDistanceExceed) { + FmStation.cleanSearchedStations(mContext); + for (int j = 0; j < searchedListSize; j++) { + int freqSearched = stations[j]; + if (FmUtils.isValidStation(freqSearched) && + !FmStation.isFavoriteStation(mContext, freqSearched)) { + FmStation.insertStationToDb(mContext, freqSearched, null); + } + } + } else { + // get stations from db + stationNum = updateDBInLocation(stations); + } + } + + Log.d(TAG, "updateStations.firstValidstation:" + firstValidstation + + ",stationNum:" + stationNum); + return (new int[] { + firstValidstation, stationNum + }); + } + + /** + * update DB, keep favorite and rds which is searched this time, + * delete rds from db which is not searched this time. + * @param stations + * @return number of valid searched stations + */ + private int updateDBInLocation(int[] stations) { + int stationNum = 0; + int searchedListSize = stations.length; + ArrayList<Integer> stationsInDB = new ArrayList<Integer>(); + Cursor cursor = null; + try { + // get non favorite stations + cursor = mContext.getContentResolver().query(Station.CONTENT_URI, + new String[] { FmStation.Station.FREQUENCY }, + FmStation.Station.IS_FAVORITE + "=0", + null, FmStation.Station.FREQUENCY); + if ((null != cursor) && cursor.moveToFirst()) { + + do { + int freqInDB = cursor.getInt(cursor.getColumnIndex( + FmStation.Station.FREQUENCY)); + stationsInDB.add(freqInDB); + } while (cursor.moveToNext()); + + } else { + Log.d(TAG, "updateDBInLocation, insertSearchedStation cursor is null"); + } + } finally { + if (null != cursor) { + cursor.close(); + } + } + + int listSizeInDB = stationsInDB.size(); + // delete station if db frequency is not in searched list + for (int i = 0; i < listSizeInDB; i++) { + int freqInDB = stationsInDB.get(i); + for (int j = 0; j < searchedListSize; j++) { + int freqSearched = stations[j]; + if (freqInDB == freqSearched) { + break; + } + if (j == (searchedListSize - 1) && freqInDB != freqSearched) { + // delete from db + FmStation.deleteStationInDb(mContext, freqInDB); + } + } + } + + // add to db if station is not in db + for (int j = 0; j < searchedListSize; j++) { + int freqSearched = stations[j]; + if (FmUtils.isValidStation(freqSearched)) { + stationNum++; + if (!stationsInDB.contains(freqSearched) + && !FmStation.isFavoriteStation(mContext, freqSearched)) { + // insert to db + FmStation.insertStationToDb(mContext, freqSearched, ""); + } + } + } + return stationNum; + } + + /** + * The background handler + */ + class FmRadioServiceHandler extends Handler { + public FmRadioServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Bundle bundle; + boolean isPowerup = false; + boolean isSwitch = true; + + switch (msg.what) { + + // power up + case FmListener.MSGID_POWERUP_FINISHED: + bundle = msg.getData(); + handlePowerUp(bundle); + break; + + // power down + case FmListener.MSGID_POWERDOWN_FINISHED: + handlePowerDown(); + break; + + // fm exit + case FmListener.MSGID_FM_EXIT: + if (mIsSpeakerUsed) { + setForceUse(false); + } + powerDown(); + closeDevice(); + + bundle = new Bundle(1); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.MSGID_FM_EXIT); + notifyActivityStateChanged(bundle); + // Finish favorite when exit FM + if (sExitListener != null) { + sExitListener.onExit(); + } + break; + + // switch antenna + case FmListener.MSGID_SWITCH_ANNTENNA: + bundle = msg.getData(); + int value = bundle.getInt(FmListener.SWITCH_ANNTENNA_VALUE); + + // if ear phone insert, need dismiss plugin earphone + // dialog + // if earphone plug out and it is not play recorder + // state, show plug dialog. + if (0 == value) { + // powerUpAsync(FMRadioUtils.computeFrequency(mCurrentStation)); + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.MSGID_SWITCH_ANNTENNA); + bundle.putBoolean(FmListener.KEY_IS_SWITCH_ANNTENNA, true); + notifyActivityStateChanged(bundle); + } else { + // ear phone plug out, and recorder state is not + // play recorder state, + // show dialog. + if (mRecordState != FmRecorder.STATE_PLAYBACK) { + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.MSGID_SWITCH_ANNTENNA); + bundle.putBoolean(FmListener.KEY_IS_SWITCH_ANNTENNA, false); + notifyActivityStateChanged(bundle); + } + } + break; + + // tune to station + case FmListener.MSGID_TUNE_FINISHED: + bundle = msg.getData(); + float tuneStation = bundle.getFloat(FM_FREQUENCY); + boolean isTune = tuneStation(tuneStation); + // if tune fail, pass current station to update ui + if (!isTune) { + tuneStation = FmUtils.computeFrequency(mCurrentStation); + } + bundle = new Bundle(3); + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.MSGID_TUNE_FINISHED); + bundle.putBoolean(FmListener.KEY_IS_TUNE, isTune); + bundle.putFloat(FmListener.KEY_TUNE_TO_STATION, tuneStation); + notifyActivityStateChanged(bundle); + break; + + // seek to station + case FmListener.MSGID_SEEK_FINISHED: + bundle = msg.getData(); + mIsSeeking = true; + float seekStation = seekStation(bundle.getFloat(FM_FREQUENCY), + bundle.getBoolean(OPTION)); + boolean isStationTunningSuccessed = false; + int station = FmUtils.computeStation(seekStation); + if (FmUtils.isValidStation(station)) { + isStationTunningSuccessed = tuneStation(seekStation); + } + // if tune fail, pass current station to update ui + if (!isStationTunningSuccessed) { + seekStation = FmUtils.computeFrequency(mCurrentStation); + } + bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.MSGID_TUNE_FINISHED); + bundle.putBoolean(FmListener.KEY_IS_TUNE, isStationTunningSuccessed); + bundle.putFloat(FmListener.KEY_TUNE_TO_STATION, seekStation); + notifyActivityStateChanged(bundle); + mIsSeeking = false; + break; + + // start scan + case FmListener.MSGID_SCAN_FINISHED: + int[] stations = null; + int[] result = null; + int scanTuneStation = 0; + boolean isScan = true; + mIsScanning = true; + if (powerUp(FmUtils.DEFAULT_STATION_FLOAT)) { + stations = startScan(); + } + + // check whether cancel scan + if ((null != stations) && stations[0] == -100) { + isScan = false; + result = new int[] { + -1, 0 + }; + } else { + result = updateStations(stations); + scanTuneStation = result[0]; + tuneStation(FmUtils.computeFrequency(mCurrentStation)); + } + + /* + * if there is stop command when scan, so it needs to mute + * fm avoid fm sound come out. + */ + if (mIsAudioFocusHeld) { + setMute(false); + } + bundle = new Bundle(4); + bundle.putInt(FmListener.CALLBACK_FLAG, + FmListener.MSGID_SCAN_FINISHED); + //bundle.putInt(FmListener.KEY_TUNE_TO_STATION, scanTuneStation); + bundle.putInt(FmListener.KEY_STATION_NUM, result[1]); + bundle.putBoolean(FmListener.KEY_IS_SCAN, isScan); + // Only notify the newest request activity + notifyCurrentActivityStateChanged(bundle); + mIsScanning = false; + break; + + // audio focus changed + case FmListener.MSGID_AUDIOFOCUS_CHANGED: + bundle = msg.getData(); + int focusState = bundle.getInt(FmListener.KEY_AUDIOFOCUS_CHANGED); + updateAudioFocus(focusState); + break; + + case FmListener.MSGID_SET_RDS_FINISHED: + bundle = msg.getData(); + setRds(bundle.getBoolean(OPTION)); + break; + + case FmListener.MSGID_SET_MUTE_FINISHED: + bundle = msg.getData(); + setMute(bundle.getBoolean(OPTION)); + break; + + case FmListener.MSGID_ACTIVE_AF_FINISHED: + activeAf(); + break; + + /********** recording **********/ + case FmListener.MSGID_STARTRECORDING_FINISHED: + startRecording(); + break; + + case FmListener.MSGID_STOPRECORDING_FINISHED: + stopRecording(); + break; + + case FmListener.MSGID_RECORD_MODE_CHANED: + bundle = msg.getData(); + setRecordingMode(bundle.getBoolean(OPTION)); + break; + + case FmListener.MSGID_SAVERECORDING_FINISHED: + bundle = msg.getData(); + saveRecording(bundle.getString(RECODING_FILE_NAME)); + break; + + default: + break; + } + } + + } + + /** + * handle power down, execute power down and call back to activity. + */ + private void handlePowerDown() { + Bundle bundle; + boolean isPowerdown = powerDown(); + bundle = new Bundle(1); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.MSGID_POWERDOWN_FINISHED); + notifyActivityStateChanged(bundle); + } + + /** + * handle power up, execute power up and call back to activity. + * + * @param bundle power up frequency + */ + private void handlePowerUp(Bundle bundle) { + boolean isPowerUp = false; + boolean isSwitch = true; + float curFrequency = bundle.getFloat(FM_FREQUENCY); + + if (!isAntennaAvailable()) { + Log.d(TAG, "handlePowerUp, earphone is not ready"); + bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.MSGID_SWITCH_ANNTENNA); + bundle.putBoolean(FmListener.KEY_IS_SWITCH_ANNTENNA, false); + notifyActivityStateChanged(bundle); + return; + } + if (powerUp(curFrequency)) { + if (FmUtils.isFirstTimePlayFm(mContext)) { + isPowerUp = firstPlaying(curFrequency); + FmUtils.setIsFirstTimePlayFm(mContext); + } else { + isPowerUp = playFrequency(curFrequency); + } + mPausedByTransientLossOfFocus = false; + } + bundle = new Bundle(2); + bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.MSGID_POWERUP_FINISHED); + bundle.putInt(FmListener.KEY_TUNE_TO_STATION, mCurrentStation); + notifyActivityStateChanged(bundle); + } + + /** + * check FM is foreground or background + */ + public boolean isActivityForeground() { + return (mIsFmMainForeground || mIsFmFavoriteForground); + } + + /** + * mark FmMainActivity is foreground or not + * @param isForeground + */ + public void setFmMainActivityForeground(boolean isForeground) { + mIsFmMainForeground = isForeground; + } + + /** + * mark FmFavoriteActivity activity is foreground or not + * @param isForeground + */ + public void setFmFavoriteForeground(boolean isForeground) { + mIsFmFavoriteForground = isForeground; + } + + /** + * Get the recording sdcard path when staring record + * + * @return sdcard path like "/storage/sdcard0" + */ + public static String getRecordingSdcard() { + return sRecordingSdcard; + } + + /** + * The listener interface for exit + */ + public interface OnExitListener { + /** + * When Service finish, should notify FmFavoriteActivity to finish + */ + void onExit(); + } + + /** + * Register the listener for exit + * + * @param listener The listener want to know the exit event + */ + public static void registerExitListener(OnExitListener listener) { + sExitListener = listener; + } + + /** + * Unregister the listener for exit + * + * @param listener The listener want to know the exit event + */ + public static void unregisterExitListener(OnExitListener listener) { + sExitListener = null; + } + + /** + * Get the latest recording name the show name in save dialog but saved in + * service + * + * @return The latest recording name or null for not modified + */ + public String getModifiedRecordingName() { + return mModifiedRecordingName; + } + + /** + * Set the latest recording name if modify the default name + * + * @param name The latest recording name or null for not modified + */ + public void setModifiedRecordingName(String name) { + mModifiedRecordingName = name; + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + exitFm(); + super.onTaskRemoved(rootIntent); + } + + private void volumeUp() { + if (mPatchVolume < 0) { + mPatchVolume += mPatchVolumeStep; + } + adjustAudioGain(mPatchVolume); + } + + private void volumeDown() { + if (mPatchVolume > -4500) { + mPatchVolume -= mPatchVolumeStep; + } + adjustAudioGain(mPatchVolume); + } + + private boolean firstPlaying(float frequency) { + if (mPowerStatus != POWER_UP) { + Log.w(TAG, "firstPlaying, FM is not powered up"); + return false; + } + boolean isSeekTune = false; + float seekStation = FmNative.seek(frequency, false); + int station = FmUtils.computeStation(seekStation); + if (FmUtils.isValidStation(station)) { + isSeekTune = FmNative.tune(seekStation); + if (isSeekTune) { + playFrequency(seekStation); + } + } + // if tune fail, pass current station to update ui + if (!isSeekTune) { + seekStation = FmUtils.computeFrequency(mCurrentStation); + } + return isSeekTune; + } + + /** + * Set the mIsDistanceExceed + * @param exceed true is exceed, false is not exceed + */ + public void setDistanceExceed(boolean exceed) { + mIsDistanceExceed = exceed; + } + + /** + * Set notification class name + * @param clsName The target class name of activity + */ + public void setNotificationClsName(String clsName) { + mTargetClassName = clsName; + } +} |