summaryrefslogtreecommitdiffstats
path: root/src/com/android/fmradio/FmService.java
diff options
context:
space:
mode:
authorBenson Huang <benson.huang@mediatek.com>2014-11-20 15:42:26 +0800
committerNicholas Sauer <nicksauer@google.com>2014-12-02 02:44:30 +0000
commita8b6afca0e187c008ba8fdeb670d5f2c13116bed (patch)
treefbd1cb3be95858ac34f8f2c262717e38b2b696c6 /src/com/android/fmradio/FmService.java
parentb4f696d7ce27d6ea056120f97fc36967c7178976 (diff)
downloadandroid_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.java2738
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;
+ }
+}