| /* |
| * 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.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothProfile; |
| 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.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.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.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 = "fmradio.seek.previous"; |
| private static final String FM_SEEK_NEXT = "fmradio.seek.next"; |
| private static final String FM_TURN_OFF = "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; |
| |
| // ignore audio data |
| private static final int AUDIO_FRAMES_TO_IGNORE_COUNT = 3; |
| |
| // 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 mIsFmFavoriteForeground = false; |
| // FmRecordActivity foreground |
| private boolean mIsFmRecordForeground = 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) { |
| Log.d(TAG, "setSpeakerPhoneOn " + isSpeaker); |
| setForceUse(isSpeaker); |
| } |
| |
| /** |
| * Check if BT headset is connected |
| * @return true if current is playing with BT headset |
| */ |
| public boolean isBluetoothHeadsetInUse() { |
| BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); |
| int a2dpState = btAdapter.getProfileConnectionState(BluetoothProfile.HEADSET); |
| return (BluetoothProfile.STATE_CONNECTED == a2dpState |
| || BluetoothProfile.STATE_CONNECTING == a2dpState); |
| } |
| |
| private synchronized void startRender() { |
| Log.d(TAG, "startRender " + AudioSystem.getForceUse(FOR_PROPRIETARY)); |
| |
| // need to create new audio record and audio play back track, |
| // because input/output device may be changed. |
| if (mAudioRecord != null) { |
| mAudioRecord.stop(); |
| mAudioRecord.release(); |
| mAudioRecord = null; |
| } |
| if (mAudioTrack != null) { |
| mAudioTrack.stop(); |
| mAudioTrack.release(); |
| mAudioTrack = null; |
| } |
| initAudioRecordSink(); |
| |
| mIsRender = true; |
| synchronized (mRenderLock) { |
| mRenderLock.notify(); |
| } |
| } |
| |
| private synchronized void stopRender() { |
| Log.d(TAG, "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 boolean isRendering() { |
| return mIsRender; |
| } |
| |
| private void startAudioTrack() { |
| if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) { |
| ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); |
| mAudioManager.listAudioPatches(patches); |
| mAudioTrack.play(); |
| } |
| } |
| |
| private void stopAudioTrack() { |
| if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { |
| mAudioTrack.stop(); |
| } |
| } |
| |
| class RenderThread extends Thread { |
| private int mCurrentFrame = 0; |
| private boolean isAudioFrameNeedIgnore() { |
| return mCurrentFrame < AUDIO_FRAMES_TO_IGNORE_COUNT; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| byte[] buffer = new byte[RECORD_BUF_SIZE]; |
| while (!Thread.interrupted()) { |
| if (isRender()) { |
| // Speaker mode or BT a2dp mode will come here and keep reading and writing. |
| // If we want FM sound output from speaker or BT a2dp, we must record data |
| // to AudioRecrd and write data to AudioTrack. |
| if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) { |
| mAudioRecord.startRecording(); |
| } |
| |
| if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) { |
| mAudioTrack.play(); |
| } |
| int size = mAudioRecord.read(buffer, 0, RECORD_BUF_SIZE); |
| // check whether need to ignore first 3 frames audio data from AudioRecord |
| // to avoid pop noise. |
| if (isAudioFrameNeedIgnore()) { |
| mCurrentFrame += 1; |
| continue ; |
| } |
| if (size <= 0) { |
| Log.e(TAG, "RenderThread read data from AudioRecord " |
| + "error size: " + size); |
| continue; |
| } |
| byte[] tmpBuf = new byte[size]; |
| System.arraycopy(buffer, 0, tmpBuf, 0, size); |
| // Check again to avoid noises, because mIsRender may be changed |
| // while AudioRecord is reading. |
| if (isRender()) { |
| mAudioTrack.write(tmpBuf, 0, tmpBuf.length); |
| } |
| } else { |
| // Earphone mode will come here and wait. |
| mCurrentFrame = 0; |
| |
| if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { |
| mAudioTrack.stop(); |
| } |
| |
| if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { |
| mAudioRecord.stop(); |
| } |
| |
| 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); |
| } |
| 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_ANTENNA); |
| |
| Bundle bundle = new Bundle(bundleSize); |
| bundle.putInt(FmListener.SWITCH_ANTENNA_VALUE, antenna); |
| Message msg = mFmServiceHandler.obtainMessage(FmListener.MSGID_SWITCH_ANTENNA); |
| 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(); |
| } |
| |
| private void registerAudioPortUpdateListener() { |
| if (mAudioPortUpdateListener == null) { |
| mAudioPortUpdateListener = new FmOnAudioPortUpdateListener(); |
| mAudioManager.registerAudioPortUpdateListener(mAudioPortUpdateListener); |
| } |
| } |
| |
| private void unregisterAudioPortUpdateListener() { |
| if (mAudioPortUpdateListener != null) { |
| mAudioManager.unregisterAudioPortUpdateListener(mAudioPortUpdateListener); |
| mAudioPortUpdateListener = null; |
| } |
| } |
| |
| // This function may be called in different threads. |
| // Need to add "synchronized" to make sure mAudioRecord and mAudioTrack are the newest. |
| // Thread 1: onCreate() or startRender() |
| // Thread 2: onAudioPatchListUpdate() or startRender() |
| private synchronized void initAudioRecordSink() { |
| mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.RADIO_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 int createAudioPatch() { |
| Log.d(TAG, "createAudioPatch"); |
| int status = AudioManager.SUCCESS; |
| if (mAudioPatch != null) { |
| Log.d(TAG, "createAudioPatch, mAudioPatch is not null, return"); |
| return status; |
| } |
| |
| 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}; |
| status = mAudioManager.createAudioPatch(audioPatchArray, |
| new AudioPortConfig[] {sourceConfig}, |
| new AudioPortConfig[] {sinkConfig}); |
| mAudioPatch = audioPatchArray[0]; |
| } |
| return status; |
| } |
| |
| 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) { |
| // Ingore audio port update |
| } |
| |
| /** |
| * 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, "onAudioPatchListUpdate, not power up"); |
| return; |
| } |
| |
| if (!mIsAudioFocusHeld) { |
| Log.d(TAG, "onAudioPatchListUpdate no audio focus"); |
| return; |
| } |
| |
| if (mAudioPatch != null) { |
| ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); |
| mAudioManager.listAudioPatches(patches); |
| // When BT or WFD is connected, native will remove the patch (mixer -> device). |
| // Need to recreate AudioRecord and AudioTrack for this case. |
| if (isPatchMixerToDeviceRemoved(patches)) { |
| Log.d(TAG, "onAudioPatchListUpdate reinit for BT or WFD connected"); |
| initAudioRecordSink(); |
| startRender(); |
| return; |
| } |
| if (isPatchMixerToEarphone(patches)) { |
| stopRender(); |
| } else { |
| releaseAudioPatch(); |
| startRender(); |
| } |
| } else if (mIsRender) { |
| ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); |
| mAudioManager.listAudioPatches(patches); |
| if (isPatchMixerToEarphone(patches)) { |
| int status; |
| stopAudioTrack(); |
| stopRender(); |
| status = createAudioPatch(); |
| if (status != AudioManager.SUCCESS){ |
| Log.d(TAG, "onAudioPatchListUpdate: fallback as createAudioPatch failed"); |
| startRender(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Callback method called when the mediaserver dies |
| */ |
| @Override |
| public void onServiceDied() { |
| enableFmAudio(false); |
| } |
| } |
| |
| private synchronized void releaseAudioPatch() { |
| if (mAudioPatch != null) { |
| Log.d(TAG, "releaseAudioPatch"); |
| mAudioManager.releaseAudioPatch(mAudioPatch); |
| mAudioPatch = null; |
| } |
| 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(); |
| 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(); |
| if (!mRtTextString.equals(rds)) { |
| updatePlayingNotification(); |
| } |
| 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)) { |
| int status; |
| stopAudioTrack(); |
| stopRender(); |
| status = createAudioPatch(); |
| if (status != AudioManager.SUCCESS){ |
| Log.d(TAG, "enableFmAudio: fallback as createAudioPatch failed"); |
| startRender(); |
| } |
| } 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(); |
| Log.d(TAG, "isPatchMixerToEarphone " + sourcePort + " ====> " + sinkPort); |
| 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; |
| } |
| |
| // Check whether the patch (mixer -> device) is removed by native. |
| // If no patch (mixer -> device), return true. |
| private boolean isPatchMixerToDeviceRemoved(ArrayList<AudioPatch> patches) { |
| boolean noMixerToDevice = true; |
| 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) { |
| noMixerToDevice = false; |
| break; |
| } |
| } |
| return noMixerToDevice; |
| } |
| |
| /** |
| * 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 radioText = ""; |
| 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)); |
| } |
| radioText = 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 radio text is "" or null, we also need to update notification. |
| mNotificationBuilder.setContentText(radioText); |
| Log.d(TAG, "showPlayingNotification PS:" + stationName + ", RT:" + radioText); |
| |
| 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 (FmUtils.getIsSpeakerModeOnFocusLost(mContext)) { |
| setForceUse(true); |
| FmUtils.setIsSpeakerModeOnFocusLost(mContext, false); |
| } |
| 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) { |
| Log.d(TAG, "onAudioFocusChange " + 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; |
| |
| case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: |
| synchronized (this) { |
| updateAudioFocusAync( |
| AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); |
| } |
| 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(); |
| forceToHeadsetMode(); |
| 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(); |
| forceToHeadsetMode(); |
| break; |
| |
| case AudioManager.AUDIOFOCUS_GAIN: |
| if (FmUtils.getIsSpeakerModeOnFocusLost(mContext)) { |
| setForceUse(true); |
| FmUtils.setIsSpeakerModeOnFocusLost(mContext, false); |
| } |
| 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); |
| } |
| setMute(false); |
| break; |
| |
| case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: |
| setMute(true); |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| private void forceToHeadsetMode() { |
| if (mIsSpeakerUsed && isHeadSetIn()) { |
| AudioSystem.setForceUse(FOR_PROPRIETARY, AudioSystem.FORCE_NONE); |
| // save user's option to shared preferences. |
| FmUtils.setIsSpeakerModeOnFocusLost(mContext, true); |
| } |
| } |
| |
| /** |
| * 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_ANTENNA: |
| bundle = msg.getData(); |
| int value = bundle.getInt(FmListener.SWITCH_ANTENNA_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_ANTENNA); |
| bundle.putBoolean(FmListener.KEY_IS_SWITCH_ANTENNA, 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_ANTENNA); |
| bundle.putBoolean(FmListener.KEY_IS_SWITCH_ANTENNA, 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); |
| |
| mIsScanning = false; |
| // Only notify the newest request activity |
| notifyCurrentActivityStateChanged(bundle); |
| 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_ANTENNA); |
| bundle.putBoolean(FmListener.KEY_IS_SWITCH_ANTENNA, 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 || mIsFmFavoriteForeground || mIsFmRecordForeground); |
| } |
| |
| /** |
| * 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) { |
| mIsFmFavoriteForeground = isForeground; |
| } |
| |
| /** |
| * mark FmRecordActivity activity is foreground or not |
| * @param isForeground |
| */ |
| public void setFmRecordActivityForeground(boolean isForeground) { |
| mIsFmRecordForeground = 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 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; |
| } |
| } |