Android 线控处理逻辑-线控注册过程

本文对比分析了Android 4.4与6.0中线控功能的实现方式。4.4版采用广播方式,通过PendingIntent传递;而6.0版改用MediaSession管理,采用Binder通讯,提升了线控消息的可靠性。

    【线控】:机电行业特定短语。指机电控制里边的一种物理控制方式,主要是指信号发生器与信号接收器之间的连接方式是通过线缆或其他动作传到物体进行连接的。

    Android线控:我们经常使用耳机上面的上一曲、下一曲、播放、暂停等操作。大体逻辑是耳机上的物理按键按下后会作为系统系统键值处理。之前看过android4.4的源码最近看android6.0的源码发现两者处理的方式有明显不同,本篇主要从adnroid4.4跟android6.0两套源码对比分析下线控的注册过程 后续分析源码中线控的下发逻辑。

【Android 4.4.3线控注册过程】

        一。android应用软件中线控的注册

                1.1 实现BroadcastReceiver 类,如下:

public class MediaKeyReceiverextends BroadcastReceiver {
    private String Tag = "MediaKeyReceiver001";
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) {
            KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
            switch (keyEvent.getKeyCode()) {
                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: //播放暂停
                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
                        L.i(Tag, "KEYCODE_MEDIA_PLAY_PAUSE  ACTION_UP");
                    } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
                        L.i(Tag, "KEYCODE_MEDIA_PLAY_PAUSE  ACTION_DOWN");
                    }
                    break;
                case KeyEvent.KEYCODE_MEDIA_NEXT://下一曲
                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
                        L.i(Tag, "KEYCODE_MEDIA_NEXT  ACTION_UP");
                    } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
                        L.i(Tag, "KEYCODE_MEDIA_NEXT  ACTION_DOWN");
                    }
                    break;
                case KeyEvent.KEYCODE_MEDIA_PREVIOUS://上一曲
                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
                        L.i(Tag, "KEYCODE_MEDIA_PREVIOUS  ACTION_UP");
                    } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
                        L.i(Tag, "KEYCODE_MEDIA_PREVIOUS  ACTION_DOWN");
                    }
                    break;
            }
        }
    }
}

                1.2 在AndroidManifest.xml中注册自己的BroadcastReceiver

<receiver android:name=".MediaKeyReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

                1.3 注册线控广播

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
ComponentName name = new ComponentName(getPackageName(), MediaKeyReceiver.class.getName());
audioManager.registerMediaButtonEventReceiver(name);

        二。Framework中线控的注册过程。

                    注册线控使用AudioManager的registerMediaButtonEventReceiver(ComponentName eventReceiver) ,传递一个ComponentName对象给AudioManager,源码如下:



/**
 * Register a component to be the sole receiver of MEDIA_BUTTON intents.
 *
 * @param eventReceiver identifier of a {@link android.content.BroadcastReceiver}
 *                      that will receive the media button intent. This broadcast receiver must be declared
 *                      in the application manifest. The package of the component must match that of
 *                      the context you're registering from.
 */
public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
    if (eventReceiver == null) {
        return;
    }
    if (!eventReceiver.getPackageName().equals(mContext.getPackageName())) {
        Log.e(TAG, "registerMediaButtonEventReceiver() error: " +
                "receiver and context package names don't match");
        return;
    }
    // construct a PendingIntent for the media button and register it
    Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    //     the associated intent will be handled by the component being registered
    mediaButtonIntent.setComponent(eventReceiver);
    PendingIntent pi = PendingIntent.getBroadcast(mContext,
            0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/);
    registerMediaButtonIntent(pi, eventReceiver);
}

        可以看到此处会做下判断,如果当前传递ComponentName的包名跟AudioManager的包名不一致的话不作处理(正常情况下包名都是一致的)。此处构造一个PendingIntent对象,后面我们会看到这个对象是做什么用的。



/**
 * @hide no-op if (pi == null) or (eventReceiver == null)
 */
public void registerMediaButtonIntent(PendingIntent pi, ComponentName eventReceiver) {
    if (pi == null) {
        Log.e(TAG, "Cannot call registerMediaButtonIntent() with a null parameter");
        return;
    }
    IAudioService service = getService();
    try {
        // pi != null
        service.registerMediaButtonIntent(pi, eventReceiver,
                eventReceiver == null ? mToken : null);
    } catch (RemoteException e) {
        Log.e(TAG, "Dead object in registerMediaButtonIntent" + e);
    }
}

    可以看到最后实际上调用的是AudioManager的隐藏的registerMediaButtonIntent方法将PendingIntent跟ComponentName参数传递给AudioService,AudioService又传递给MediaFocusControl

public void registerMediaButtonIntent(PendingIntent pi, ComponentName c, IBinder token) {
    mMediaFocusControl.registerMediaButtonIntent(pi, c, token);
}

      直接看MediaFocusControl中的处理



/**
 * see AudioManager.registerMediaButtonIntent(PendingIntent pi, ComponentName c)
 * precondition: mediaIntent != null
 */
protected void registerMediaButtonIntent(PendingIntent mediaIntent, ComponentName eventReceiver, IBinder token) {
    Log.i(TAG, "  Remote Control   registerMediaButtonIntent() for " + mediaIntent);
    synchronized (mAudioFocusLock) {
        synchronized (mRCStack) {
            if (pushMediaButtonReceiver_syncAfRcs(mediaIntent, eventReceiver, token)) {
                // new RC client, assume every type of information shall be queried
                checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
            }
        }
    }
}
/**
 * Helper function:
 * Set the new remote control receiver at the top of the RC focus stack.
 * Called synchronized on mAudioFocusLock, then mRCStack
 * precondition: mediaIntent != null
 *
 * @return true if mRCStack was changed, false otherwise
 */
private boolean pushMediaButtonReceiver_syncAfRcs(PendingIntent mediaIntent,
                                                  ComponentName target, IBinder token) {
    // already at top of stack?
    if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(mediaIntent)) {
        return false;
    }
    if (mAppOps.noteOp(AppOpsManager.OP_TAKE_MEDIA_BUTTONS, Binder.getCallingUid(),
            mediaIntent.getCreatorPackage()) != AppOpsManager.MODE_ALLOWED) {
        return false;
    }
    RemoteControlStackEntry rcse = null;
    boolean wasInsideStack = false;
    try {
        for (int index = mRCStack.size() - 1; index >= 0; index--) {
            rcse = mRCStack.elementAt(index);
            if (rcse.mMediaIntent.equals(mediaIntent)) {
                // ok to remove element while traversing the stack since we're leaving the loop
                mRCStack.removeElementAt(index);
                wasInsideStack = true;
                break;
            }
        }
    } catch (ArrayIndexOutOfBoundsException e) {
        // not expected to happen, indicates improper concurrent modification
        Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e);
    }
    if (!wasInsideStack) {
        rcse = new RemoteControlStackEntry(this, mediaIntent, target, token);
    }
    mRCStack.push(rcse); // rcse is never null

    // post message to persist the default media button receiver
    if (target != null) {
        mEventHandler.sendMessage(mEventHandler.obtainMessage(
                MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0, target/*obj*/));
    }

    // RC stack was modified
    return true;
}

    可以看到当前线控消息已经在保存的栈并且在栈顶时不作处理,如果没有在栈顶,则移除站内的线控消息同时创建新的线控对象保存在栈顶。到此线控的注册过程完成。

【Android 6.0.1线控注册过程】

  

  一。android应用软件中线控的注册

       通过android4.4的注册过程看到是通过广播方式来接收线控消息,通过PendingIntent传递,可能因为广播本身是不可靠的通讯方式所以android5.0后修改为Binder来通讯,通过MediaSession来管理。

        1.1 MediaSession的初始化

               

public class MediaKeyService extends Service {
    private MediaSession mediaSession;
    private AudioManager audioManager;
    @RequiresApi(api = 26)
    @Override
    public void onCreate() {
        super.onCreate();
        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        initMediaSession();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mediaSession!=null){
            mediaSession.release();
        }
    }

    private void play(){
        //自己的播放逻辑,需要更新MediaSession状态
        updatePlayState(false);
    }

    private void pause(){
        //自己的暂停逻辑,需要更新MediaSession状态
        updatePlayState(true);
    }

    private void updatePlayState(boolean isPlaying){
        if (mediaSession==null)
            return;
        PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
                .setActions(PlaybackState.ACTION_PLAY
                        | PlaybackState.ACTION_PLAY_PAUSE
                        | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID
                        | PlaybackState.ACTION_PAUSE
                        | PlaybackState.ACTION_SKIP_TO_NEXT
                        | PlaybackState.ACTION_SKIP_TO_PREVIOUS);
        if (isPlaying) {
            stateBuilder.setState(PlaybackState.STATE_PLAYING,
                    PlaybackState.PLAYBACK_POSITION_UNKNOWN,
                    SystemClock.elapsedRealtime());
        } else {
            stateBuilder.setState(PlaybackState.STATE_PAUSED,
                    PlaybackState.PLAYBACK_POSITION_UNKNOWN,
                    SystemClock.elapsedRealtime());
        }
        mediaSession.setPlaybackState(stateBuilder.build());
    }

    private void initMediaSession() {
        mediaSession = new MediaSession(this, "com.android.music");
        mediaSession.setCallback(new MediaSession.Callback() {

            @Override
            public void onSkipToPrevious() { //上一曲

            }

            @Override
            public void onSkipToNext() {//下一曲

            }

            @Override
            public void onPlay() {//播放

            }

            @Override
            public void onPause() {//暂停

            }
        });
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);

 } AudioManager.OnAudioFocusChangeListener audioFocusLinister = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_LOSS://焦点被别的应用抢占,失去焦点此时不再接收线控消息 mediaSession.setActive(false); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT://短暂的失去音频焦点此时不再接收线控消息 mediaSession.setActive(false); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK://焦点会短暂失去,但是可以继续压低音量播放,如果是压低音量播放此处可以继续接收线控消息 mediaSession.setActive(true); break; case AudioManager.AUDIOFOCUS_GAIN://完全获得焦点,接收线控消息 mediaSession.setActive(true); break; default: } } }; }

    以上为伪代码,在Service的onCreate中初始化MediaSession在onDestory()中注销,当需要监听线控消息的时候调用 mediaSession.setActive(true)接口来激活,不需要监听线控消息的时候调用 mediaSession.setActive(false)来注销。因为线控消息一般跟AudioFocus同步,即获取焦点的时候激活线控回调,失去焦点时注销。所以我在AudioFocus的回调中处理。

    细心的同学会注意到代码里面有更新当前的播放状态updatePlayState(),可能有疑问为什么要将当前的播放状态传递给MediaSession。一般手机线控播放暂停是同一个按键,在android 4.4直接接收的是按键值,KEYCODE_MEDIA_PLAY_PAUSE键代表了播放暂停键需要根据自己当前的状态来做处理,在android 6.0中没有播放暂停的接口,所以需要将自己当前播放状态上传至MediaSession中,在消息分发过程中会判断当前的播放状态来分发。

  二。Framework中线控的注册过程

        

public MediaSession(@NonNull Context context, @NonNull String tag, int userId) {
    if (context == null) {
        throw new IllegalArgumentException("context cannot be null.");
    }
    if (TextUtils.isEmpty(tag)) {
        throw new IllegalArgumentException("tag cannot be null or empty");
    }
    mMaxBitmapSize = context.getResources().getDimensionPixelSize(
            com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize);
    mCbStub = new CallbackStub(this);
    MediaSessionManager manager = (MediaSessionManager) context
            .getSystemService(Context.MEDIA_SESSION_SERVICE);
    try {
        mBinder = manager.createSession(mCbStub, tag, userId);
        mSessionToken = new Token(mBinder.getController());
        mController = new MediaController(context, mSessionToken);
    } catch (RemoteException e) {
        throw new RuntimeException("Remote error creating session.", e);
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值