Android Music Play Mode Switching - External Play, Hearing, Earphone

Keywords: MediaPlayer Android Mobile

Links to the original text: https://blog.csdn.net/u010936731/article/details/70599482/

Android Music Play Mode Switching - External Play, Hearing, Earphone
https://blog.csdn.net/u010936731/article/details/70599482/
Scenario requirements
In the chat scene, when receiving the voice of the other party, the user can choose to play outside or insert headphones to listen. More humanized, when the user is close to the ear, the screen closes and automatically switches to the receiver to play. After playing, the mobile screen automatically lights up. For example, Wechat.

requirement analysis
From the above scenario, we can draw the main points we need:
Play mode switching: Output <-> headset
Play Mode Switching: External Player <-> Receiver
Screen operation: bright screen <-> breath screen <-> bright screen

Solve the problem
From the requirement analysis, we can conclude that the code control is needed:
Music Play Control
Switching between Output, Earphone and Hearing
Screen Screen and Bright Screen

Music Play Control
Music playback control is the simplest. MediaPlayer can be used directly. In order to better separate from the interface code and better control music, a control class is written here: Player Manager, as follows:

/**
 * Music Playing Management Category
 */
public class PlayerManager {

    private static PlayerManager playerManager;

    private MediaPlayer mediaPlayer;
    private PlayCallback callback;
    private Context context;

    private String filePath;

    public static PlayerManager getManager(){
        if (playerManager == null){
            synchronized (PlayerManager.class){
                playerManager = new PlayerManager();
            }
        }
        return playerManager;
    }

    private PlayerManager(){
        this.context = MyApplication.getContext();
        mediaPlayer = new MediaPlayer();
    }

    /**
     * Playback callback interface
     */
    public interface PlayCallback{

        /** Music ready */
        void onPrepared();

        /** Music playback completed */
        void onComplete();

        /** Music stops playing */
        void onStop();
    }

    /**
     * Play music
     * @param path Music File Path
     * @param callback Playback callback function
     */
    public void play(String path, final PlayCallback callback){
        this.filePath = path;
        this.callback = callback;
        try {
            mediaPlayer.reset();
            mediaPlayer.setDataSource(context, Uri.parse(path));
            mediaPlayer.prepare();
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    callback.onPrepared();
                    mediaPlayer.start();
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * stop playing
     */
    public void stop(){
        if (isPlaying()){
            try {
                mediaPlayer.stop();
                callback.onStop();
            } catch (IllegalStateException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Is it playing now?
     * @return Return true while playing, otherwise return false
     */
    public boolean isPlaying() {
        return mediaPlayer != null && mediaPlayer.isPlaying();
    }
}

//To facilitate the retrieval of Context, the Application class is overwritten as follows:

/**
 * APP Application
 */
public class MyApplication extends Application {

    private static Context context;

    @Override
    public void onCreate() {
        super.onCreate();
        context = this;
    }

    /**
     * Get the Contentext of APP to facilitate calls elsewhere
     * @return
     */
    public static Context getContext(){
        return context;
    }
}

Switching between Output, Earphone and Hearing
In Android system, Audio Manager is used to manage the playback mode, which is implemented by Audio Manager. setMode ().
In the setMode() method, there are several corresponding playback modes:

MODE_NORMAL: Normal mode, neither ringtone mode nor call mode
MODE_RINGTONE: Ring mode
MODE_IN_CALL: Call Mode
MODE_IN_COMMUNICATION: Communication mode, including audio/video, VoIP calls. (3.0 added, similar to call mode)
Among them:
Playing music corresponds to MODE_NORMAL. If you use outcast, call audioManager.setSpeakerphoneOn(true).
If you use headphones and earphones, you need to set the mode to MODE_IN_CALL (before 3.0) or MODE_IN_COMMUNICATION (after 3.0).

Be careful:
Require permission android.permission.MODIFY_AUDIO_SETTINGS
Why set the mode to MODE_IN_COMMUNICATION after 3.0 instead of MODE_IN_CALL?
It has been proved that MODE_IN_CALL does not work at all in some Huawei models.
So hold an AudioManager variable in the PlayerManager class and add the following methods:

audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

/**
 * Switch to Output
 */
public void changeToSpeaker(){
    audioManager.setMode(AudioManager.MODE_NORMAL);
    audioManager.setSpeakerphoneOn(true);
}

/**
 * Switch to headphone mode
 */
public void changeToHeadset(){
    audioManager.setSpeakerphoneOn(false);
}

/**
 * Switch to the receiver
 */
public void changeToReceiver(){
    audioManager.setSpeakerphoneOn(false);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
    } else {
        audioManager.setMode(AudioManager.MODE_IN_CALL);
    }
}

How to determine whether the user plugs in the headset?
When the earphone is inserted or pulled out, the system will issue an Action broadcast for Intent.ACTION_HEADSET_PLUG, and the broadcast can not be processed by a static receiver, so it is enough to write a broadcast receiver to handle the earphone event.

class HeadsetReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        switch (action){
            //Inserting and pulling out headphones triggers this broadcast
            case Intent.ACTION_HEADSET_PLUG:
                int state = intent.getIntExtra("state", 0);
                if (state == 1){
                    playerManager.changeToHeadset();
                } else if (state == 0){
                    playerManager.changeToSpeaker();
                }
                break;
            default:
                break;
        }
    }
}

Screen Screen and Bright Screen
Screen screens and bright screens have a premise is to correctly judge whether the user is close to the receiver, how to judge?
Almost every cell phone now has a distance sensor, which is managed by Sensor Manager.

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);

The last parameter of the method of registered monitoring is sensitivity. The higher the sensitivity, the more power consumption. Select general sensitivity here. In addition, Activity needs to implement Sensor EventListener interface to override its method:

@Override
public void onSensorChanged(SensorEvent event) {
    float value = event.values[0];

    if (playerManager.isPlaying()){
        if (value == sensor.getMaximumRange()) {
            playerManager.changeToSpeaker();
            setScreenOn();
        } else {
            playerManager.changeToReceiver();
            setScreenOff();
        }
    } else {
        if(value == sensor.getMaximumRange()){
            playerManager.changeToSpeaker();
            setScreenOn();
        }
    }
}

In Android system, the control of the working state of hardware is controlled by PowerManager and WakeLock. PowerManager controls the working state of CPU, screen, keyboard and other hardware through different WakeLock.

powerManager = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);

Note: permissions android.Manifest.permission.DEVICE_POWER and android.permission.WAKE_LOCK are required
The first parameter represents the control level, and the optional values are:

PARTIAL_WAKE_LOCK: CPU running, screen and keyboard may be closed
 SCREEN_DIM_WAKE_LOCK: The screen is on and the keyboard lamp may be off
 SCREEN_BRIGHT_WAKE_LOCK: The screen is fully lit and the keyboard lamp may be turned off
 FULL_WAKE_LOCK: Screen and keyboard lights are all on
 PROXIMITY_SCREEN_OFF_WAKE_LOCK: Screen closed, keyboard lamp closed, CPU running
 DOZE_WAKE_LOCK: Screen gray, CPU delayed work
 Here we select 5.PROXIMITY_SCREEN_OFF_WAKE_LOCK.WakeLock to lock and unlock by acqui() and release().
private void setScreenOff(){
    if (wakeLock == null){
        wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
    }
    wakeLock.acquire();
}

private void setScreenOn(){
    if (wakeLock != null){
        wakeLock.setReferenceCounted(false);
        wakeLock.release();
        wakeLock = null;
    }
}

Begin validation
Through the above three solutions, and then running the program, we can see that the basic functional requirements are met. But there are the following problems:

The hand-shielded distance sensor switches to the receiver in headphone mode
Samsung Note, Huawei P, Huawei Mate series will switch to the receiver, and the receiver will switch to the receiver, which will lead to the Karton phenomenon.
Loss of voice will occur when headphones are switched to external amplifiers
Samsung, Huawei Mobile Phone will call Activity's onPause(),onStop() method when the screen is turned off!
Solving new problems
The Problem of Hand Blocking Distance Sensor in Earphone Mode
This problem can only be solved by not responding to the distance sensor in the headset mode, and adding in Player Manager:

/**
 * Is Headphone Inserted
 * @return Insert headphones to return true, otherwise return false
 */
@SuppressWarnings("deprecation")
public boolean isWiredHeadsetOn(){
    return audioManager.isWiredHeadsetOn();
}

Then the method of distance sensor callback is modified as follows:

@Override
public void onSensorChanged(SensorEvent event) {
    float value = event.values[0];

    if (playerManager.isWiredHeadsetOn()){
        return;
    }

    if (playerManager.isPlaying()){
        if (value == sensor.getMaximumRange()) {
            playerManager.changeToSpeaker();
            setScreenOn();
        } else {
            playerManager.changeToReceiver();
            setScreenOff();
        }
    } else {
        if(value == sensor.getMaximumRange()){
            playerManager.changeToSpeaker();
            setScreenOn();
        }
    }
}

Samsung, Huawei Output Switching Cardon
This problem can only be compromised: replay
Why does this approach work?

Short voice is inherently short and handover replay is almost unaffected.
Music usually doesn't listen to on the receiver.
Not all mobile phones will have Carton.
So modify the method in Player Manager:

/**
 * Switch to the receiver
 */
public void changeToReceiver(){
    if (isPlaying()){
        stop();
        changeToReceiverNoStop();
        play(filePath, callback);
    } else {
        changeToReceiverNoStop();
    }
}

/**
 * Switch to Output
 */
public void changeToSpeaker(){
    if (PhoneModelUtil.isSamsungPhone() || PhoneModelUtil.isHuaweiPhone()){
        stop();
        changeToSpeakerNoStop();
        play(filePath, callback);
    } else {
        changeToSpeakerNoStop();
    }
}

public void changeToSpeakerNoStop(){
    audioManager.setMode(AudioManager.MODE_NORMAL);
    audioManager.setSpeakerphoneOn(true);
}

Loss of voice will occur when headphones are switched to external amplifiers
This problem is caused by the fact that it takes some time for the headset to switch to the external amplifier, so the solution to this problem is to suspend the replay first and then. When and when will the replay be suspended?
According to the information, when the headset is pulled out, the system will also issue an Action broadcast for Audio Manager. Action_AUDIO_BECOMING_NOISY, and this broadcast is earlier than Intent.ACTION_HEADSET_PLUG, so the solution comes out:

Pause play when AudioManager.ACTION_AUDIO_BECOMING_NOISY is received
Upon receipt of Intent.ACTION_HEADSET_PLUG with state=1

Samsung and Huawei phones call Activity's onPause(),onStop() methods when the screen is turned off.
It's not a problem, but it's worth noting. If you do some resource release operations in onStop(), you need to retrieve them in onStart() to prevent other problems.

Posted by Jyotsna on Sat, 07 Sep 2019 05:03:51 -0700