前阵子,刚好遇到个问题:
收听FM,当前频道显示为105.3,拔出耳机.在FM界面选择89.9频道,再次插入耳机收听的FM不是选中的89.9频道,而是105.3频道。 |
这个的根本原因是拔出耳机后,fmr会监听action为ACTION_HEADSET_PLUG 的广播,然后在activity中禁用相关的view控件(这些控件只能在插入耳机,也就是能正常使用收音机的情况下使用)。但是由于广播会延迟,拔出耳机后并不是立即就会收到广播,而且相关的方法处理也会耗时,所以,拔出耳机后的二到三秒之间是可以操作view的,这个view被用户操作后,值就会变化,但在service解除绑定并被销毁的时候,有些数据是没有被保存的。
因此,当再一次插入耳机之后,按照之前的配置还原,有一定的误差。
解决办法: 可以使用,当拔出耳机后直接退出fm,但这样貌似不太友好,可能客户只是不小心拔出了呢?另外就是处理如何规避广播延迟的影响呢?
基于这个问题,特地看了下fmr的源代码。没有具体去看,只看了个大概。
收音机插拔耳机处理
当耳机插入和拔出,framework层AudioService.java都有广播发出。
对应的广播Action(in Intent.java, android.media.AudioManager.java)
public static final String ACTION_HEADSET_PLUG =android.media.AudioManager.ACTION_HEADSET_PLUG;
public static final String ACTION_HEADSET_PLUG =
"android.intent.action.HEADSET_PLUG";
Fmr在FMRadioService.java中注册耳机插拔的广播。
插拔耳机的广播intent,会携带两个参数,state 取值 0 拔出;1 插入 ,micphone 1 携带micphone的耳机,0 不携带micphone的耳机。
<span style="font-size:12px;">mReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action == null) { return; } Log.i(TAG, "Received intent: " + action); //#########ACTION_HEADSET_PLUG begin if (action.equals(Intent.ACTION_HEADSET_PLUG)) { if (mFMServiceState != FM_SERVICE_ON) { //the service is running good. return; } final boolean headsetPlugin = (intent.getIntExtra("state", 0) == 1);//in 1 ,out 0 final boolean supportInAntenna = Util .supportInternalAntenna(context);//headset model if (DEBUG) Log.d(TAG, "HEADSET is pluged in? : " + headsetPlugin); if (!supportInAntenna && !headsetPlugin) { // The headset is plugged out, stop FMR. Intent Stopintent = new Intent( "android.intent.action.STOP_FM"); sendBroadcast(Stopintent); mFMServiceState = FM_SERVICE_OPENED;//when exit the fm //update the widget . if (mAppWidgetProvider != null) { mAppWidgetProvider.notifyChange( FMRadioService.this, FM_CLOSED); } /*enable touch sound when headset plugged out, add by RC */ mAudioInterface.enableTouchSound(); /*enable touch sound when headset plugged out, add by RC */ clearFMService();// >>> Toast.makeText(FMRadioService.this, R.string.fmradio_no_headset_in_listen, Toast.LENGTH_LONG).show(); //stopSelf(mServiceStartId); }</span>
当FMRadioService.java接收到广播之后,会停止所有活动包括Service。但是,广播有一定的延迟,可能抽出耳机之后,UI界面在短时间内仍然可以操作(大概2s-5s)。
在注册广播代码中,
iFilter.addAction(Intent.ACTION_HEADSET_PLUG);
iFilter.addAction(FMAudioInterface.FM_ROUTE_HEADSET);
iFilter.addAction(FMAudioInterface.FM_ROUTE_LOUDSPEAKER);
有FM_ROUTE_HEADSET和FM_ROUTE_LOUDSPEAKER 与耳机类型相关,耳机插拔后是否允许使用扬声器,通常是只能插入耳机才能收听。
二,监听sd卡插拔
In Intent.java
public static final String ACTION_MEDIA_UNMOUNTED ="android.intent.action.MEDIA_UNMOUNTED"
<span style="font-size:12px;">mSdCardListener = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action == null) { return; } if (DEBUG) Log.i(TAG, "Received intent: " + action); if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { if (mFMServiceState == FM_SERVICE_ON) { Intent Stopintent = new Intent( "android.intent.action.STOP_FM"); sendBroadcast(Stopintent); } } } };</span>
三 ,aidl (androidinterface definition language)
FMRadio.java
FMRadioService.java
IFMRadioService.aidl
IRemoteServiceCallBack.aidl
AIDL文件主要声明相关接口,供client使用
而这些接口是在对应Service中实现的(Server)。
需要的几个步骤:
1. 创建AIDL文件,和java包在同一个包下面。
2. 对应service实现Stub
3. 客户端使用的Activity创建ServiceConnection,绑定
下面查看fmr中是如何实现的Service和Activity通信的。
FMRService.java 内部实现对应IFMRadioService.Stub()
// Implementation of IFMRadioService AIDL Interface private final IFMRadioService.Stub mBinder = new IFMRadioService.Stub() { public int getFMServiceStatus() { if (DEBUG) Log.i(TAG, "AIDL: getFMServiceStatus: " + mFMServiceState); return mFMServiceState; } …… }
该binder对象和Service绑定
@Override public IBinder onBind(Intent intent) { if (DEBUG) Log.i(TAG, "onBind() called"); return mBinder; }
@Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate()"); super.onCreate(savedInstanceState); …… // bind to FMRadioService bindToService(); …… }
另外:
我们在绑定activity到service的时候,调用bindService方法,第一个参数中是个intent意图,参数是需要绑定的Service的报名+类名,第二个参数Context.BIND_AUTO_CREATE 为自动绑定机制,即使你不创建也会绑定。
private boolean bindToService() { Log.i(TAG, "Start to bind to FMRadio service"); return bindService(new Intent("com.pekall.fmradio.FMRADIO_SERVICE"), mServConnection, Context.BIND_AUTO_CREATE); }
/#######test ###### bind private ServiceConnection mServConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, android.os.IBinder service) { Log.w(TAG, "onServiceConnected"); mIsBound = true; //initialize FMRadioService //创建IFMRadioService对象 mFMService = IFMRadioService.Stub.asInterface(service); if (mFMService == null) { Log.e(TAG, "onServiceConnected error, mFMService null"); return; } updateTrackInfo(); updateDisplayPanel(mCurrentFreq); updateSwithcButton(); try { mFMService.registerCallback(mCallback); } catch (RemoteException e) { Log.e(TAG, "", e); } } public void onServiceDisconnected(ComponentName className) { Log.i(TAG, "onServiceDisconnected"); if (mFMService == null) { Log.e(TAG, "onServiceDisconnected error, mFMService null"); return; } try { mFMService.unregisterCallback(mCallback); } catch (RemoteException e) { Log.e(TAG, "", e); } mFMService = null; finish(); } };
获取手机存储列表。internal or sdcar
在fmr源码中,看到有个方式是获取手机的存储列表路径。
源代码如下,SDCardInfo.java
private class SDCardInfo { public static final int INTERNAL_SD = 1; public static final int EXTERNAL_SD = 2; String path; String state; int type; public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getState() { return state; } public void setState(String state) { this.state = state; } public int getType() { return type; } public void setType(int type) { this.type = type; } boolean isMounted() { if (Environment.MEDIA_MOUNTED.equals(this.state)) { return true; } return false; } public String toString() { return "[ SDCardInfo: path=" + path + ", state=" + state + ", isMounted =" + isMounted() + ", type is " + type + "]"; } }
private ArrayList<SDCardInfo> initSDCardList() { StorageManager storageManager = (StorageManager) FMRadio.this.getSystemService(Context.STORAGE_SERVICE); ArrayList<SDCardInfo> sdCardInfos = new ArrayList<SDCardInfo>(); StorageVolume[] storgerVolumeList = storageManager.getVolumeList(); String[] storgerPaths = storageManager.getVolumePaths(); if (storgerVolumeList != null) { for (int i = 0; i < storgerVolumeList.length; i++) { SDCardInfo sdCardInfo = new SDCardInfo(); sdCardInfo.path = storgerPaths[i]; boolean isCanRemoved = storgerVolumeList[i].isRemovable(); if (isCanRemoved) { sdCardInfo.type = SDCardInfo.EXTERNAL_SD; } else { sdCardInfo.type = SDCardInfo.INTERNAL_SD; } sdCardInfo.state = storageManager.getVolumeState(storgerPaths[i]); sdCardInfos.add(sdCardInfo); } } Log.i(TAG, "getExternalStorageDirectory = " + Environment.getExternalStorageDirectory()); Log.i(TAG, "sdCard info = " + sdCardInfos); return sdCardInfos; }
public String[] getPaths(Context ctx) { StorageManager manager = (StorageManager) ctx .getSystemService(Context.STORAGE_SERVICE); String path[] = null; try { Method method = manager.getClass().getMethod("getVolumePaths"); path = (String[]) method.invoke(manager); } catch (NoSuchMethodException e) { // TODO Auto-generated catch block e.printStackTrace(); Log.e("getPaths...<<<", "No such method name .<<<<"); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); Log.e("getPaths...<<<", "IllegalArgumentException .<<<<"); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); Log.e("getPaths...<<<", "IllegalAccessException .<<<<"); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); Log.e("getPaths...<<<", "InvocationTargetException .<<<<"); } return path; }