Mobile Development 14 min read

Implementing Wired Controls and Lock‑Screen Metadata Display in Android

The article details how Android music apps implement wired headset controls and lock‑screen metadata—using AudioManager and RemoteControlClient for pre‑5.0 devices and MediaSession/MediaSessionCompat for newer versions—while covering focus contention, custom lock‑screen handling, MIUI lyric integration, and other common pitfalls.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Implementing Wired Controls and Lock‑Screen Metadata Display in Android

Most mainstream Android music players support wired control (headset buttons) and lock‑screen metadata display. Wired controls work with wired headsets, Bluetooth headsets, or car Bluetooth units, allowing play/pause and track changes without touching the phone.

When music is playing, some phones (e.g., Xiaomi) show song title, artist, album art, and even lyrics on the lock‑screen, together with playback controls. The article explains how these features are implemented and the pitfalls involved.

AudioManager with RemoteControlClient (pre‑Android 5.0)

Before Android 5.0, the recommended way was to use AudioManager for wired control and RemoteControlClient for lock‑screen metadata.

mAudioManager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);

    mComponentName = new ComponentName(mContext.getPackageName(), MediaButtonReceiver.class.getName());
    mContext.getPackageManager().setComponentEnabledSetting(mComponentName,
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);

    Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    mediaButtonIntent.setComponent(mComponentName);
    mPendingIntent = PendingIntent.getBroadcast(mContext, 0, mediaButtonIntent, PendingIntent.FLAG_CANCEL_CURRENT);
    mAudioManager.registerMediaButtonEventReceiver(mComponentName);

The call to registerMediaButtonEventReceiver grabs the wired‑control focus; only one app can hold it at a time.

Declare the receiver in AndroidManifest.xml :

Handle the incoming KeyEvent inside the receiver by checking event.getKeyCode() (e.g., KeyEvent.KEYCODE_HEADSETHOOK , KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ) and event.getAction() ( KeyEvent.ACTION_UP/DOWN ).

When finished, unregister with mAudioManager.unregisterMediaButtonEventReceiver(mComponentName) .

To show lock‑screen info, create and register a RemoteControlClient :

mRemoteControlClient = new RemoteControlClient(mPendingIntent);
    mAudioManager.registerRemoteControlClient(mRemoteControlClient);

Specify which transport keys you want to handle:

int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
            | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
            | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
            | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
            | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
            | RemoteControlClient.FLAG_KEY_MEDIA_RATING;
    mRemoteControlClient.setTransportControlFlags(flags);

Send song metadata:

// playback state
    mRemoteControlClient.setPlaybackState(getPlayState(), MusicUtil.getCursongTime() * 1000, 1.0f);

    // metadata
    MetadataEditor md = mRemoteControlClient.editMetadata(true);
    md.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, song.getName());
    md.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, song.getSinger());
    md.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, song.getAlbum());
    md.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, song.getDuration());
    md.putBitmap(MetadataEditor.BITMAP_KEY_ARTWORK, mAlbumCover);
    // MIUI lyric key (int 1000)
    md.putString(1000, mLyric.toLrcString());
    md.apply();

Unregister with mAudioManager.unregisterRemoteControlClient(mRemoteControlClient) when done.

MediaSession (Android 5.0 and later)

From Android 5.0 onward, RemoteControlClient is deprecated. The modern API is MediaSession (or MediaSessionCompat for backward compatibility). It unifies wired‑control handling and lock‑screen metadata.

// set up MediaButtonReceiver again
    mComponentName = new ComponentName(mContext.getPackageName(), MediaButtonReceiver.class.getName());
    mContext.getPackageManager().setComponentEnabledSetting(mComponentName,
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);

    Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    mediaButtonIntent.setComponent(mComponentName);
    mPendingIntent = PendingIntent.getBroadcast(mContext, 0, mediaButtonIntent, PendingIntent.FLAG_CANCEL_CURRENT);

    // ensure callbacks run on the main thread
    mHandler = new Handler(Looper.getMainLooper());

    mMediaSession = new MediaSessionCompat(mContext, "mbr", mComponentName, null);
    mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    mMediaSession.setMediaButtonReceiver(mPendingIntent);

    PlaybackStateCompat state = new PlaybackStateCompat.Builder().setActions(
            PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY
                    | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
                    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_STOP).build();
    mMediaSession.setPlaybackState(state);

    mMediaSession.setCallback(new MediaSessionCompat.Callback() {
        @Override
        public boolean onMediaButtonEvent(Intent intent) {
            MediaButtonReceiver mMediaButtonReceiver = new MediaButtonReceiver();
            mMediaButtonReceiver.onReceive(mContext, intent);
            return true;
        }
    }, mHandler);

    if (!mMediaSession.isActive()) {
        mMediaSession.setActive(true);
    }

Two important notes:

Pre‑5.0 and post‑5.0 devices use different registration paths; both are kept for compatibility.

MediaSessionCompat is not thread‑safe, so all calls must be on the same (main) thread, whereas the platform MediaSession is thread‑safe.

After activation, update playback state and metadata:

// playback state
    PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder();
    stateBuilder.setState(getPlayState(), getCurrentPlayTime(), 1.0f);
    mMediaSession.setPlaybackState(stateBuilder.build());

    // metadata
    MediaMetadataCompat.Builder md = new MediaMetadataCompat.Builder();
    md.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getName());
    md.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getSinger());
    md.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbum());
    md.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration());
    md.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, mAlbumCover);
    mMediaSession.setMetadata(md.build());

Release the session with mMediaSession.release() when no longer needed.

Common Pitfalls

1. Wired‑control focus contention

Only one app can hold the wired‑control focus. If another app grabs it, your app stops receiving events. Best practice: request focus when playback starts or resumes, and also request audio focus simultaneously; loss of audio focus usually indicates loss of wired‑control focus as well.

2. Dual lock‑screen handling

Some apps provide a custom lock‑screen UI. To avoid showing both the system and custom lock‑screens, toggle the MediaSession active state:

mMediaSession.setActive(false); // hide system lock‑screen
    // later
    mMediaSession.setActive(true);  // show it again

Note that disabling the session also disables wired‑control handling, so you may need to re‑activate the session after switching.

3. MIUI lock‑screen lyrics

MIUI expects the lyric text under an integer key (1000). With MediaSessionCompat , you must convert that integer key to the corresponding string key via the hidden method MediaMetadata.getKeyFromMetadataEditorKey(int) (accessed via reflection) and then put the lyric string:

MediaMetadataCompat.Builder md = new MediaMetadataCompat.Builder();
    try {
        Class
MediaMetadataClass = Class.forName(MediaMetadata.class.getName());
        Method getKeyMethod = MediaMetadataClass.getMethod("getKeyFromMetadataEditorKey", int.class);
        String lyricKey = (String) getKeyMethod.invoke(null, 1000);
        md.putString(lyricKey, mLyric.toLrcString());
    } catch (Exception e) {
        e.printStackTrace();
    }
    mMediaSession.setMetadata(md.build());

This workaround enables lyric display on MIUI lock‑screens.

Summary

Using MediaSession (or MediaSessionCompat ) provides a modern, extensible way to handle wired controls and lock‑screen metadata in Android music apps. It supports dynamic updates such as scrolling lyrics or rotating album art, and works on Android TV via the Now Playing Card. However, developers must handle OEM‑specific customizations (e.g., MIUI) and be aware of thread‑safety and focus‑management issues, which can increase integration effort.

AndroidAudioManagerLockScreenMediaSessionRemoteControlClientWiredControl
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.