[GUIDE] Implement your own lockscreen-like music controls

Search This thread

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
Beginner developers, beware! This guide isn't beginner-friendly at all and it's targeted at developers who have some knowledge about Android development.

Or you can use my new library - Remote Metadata Provider, it's MUCH simplier to use.

0. The Introduction
You guys probably seen my apps - Floating Music Widget and Android Control Center.

They both share one feature - good music player integration. They can show you metadata and Floating Music Widget even shows album art. While some players provide API for external music controls(like PowerAmp), the others just somehow integrate with lockscreen. How? Sit down, get a cup of tea, and listen to me.

With the API Level 14 Google introduced class called RemoteControlClient. Citing Google API Reference:
RemoteControlClient enables exposing information meant to be consumed by remote controls capable of displaying metadata, artwork and media transport control buttons.

I won't explain how this works - you may go and read some tutorials around the web, there are plenty of them.
Or check API Reference here.

But. Well, we send metadata and album art. Oh, and on 4.3 we can even send playback position. However...how do we receive it? Well, by some reason, I don't know exactly why, Google has hidden this part of API. Maybe they think it's unsere to let you consume other app data, or maybe they just forgot about it. I've asked them multiple times, why did they hid this part of API, but they just ignored me.
So, by posting this article, I hope to maybe somehow make them change their minds and publish this API after all.


1. Getting started
Please note that this guide won't give you Activity examples, or any other things. It will give you the bare bones of the implementation of your own media controls. It's NOT intended to be used by Android/Java newbies.

PLEASE NOTE THAT IT'S A CLOSED API! IT MAY MALFUNCTION OR NOT WORK AT ALL!


Of course, you will need Eclipse IDE.
Also you will need modified Android build platform with hidden and internal API enabled.
There's an excellent guide on how to do this:
Using internal (com.android.internal) and hidden (@hide) APIs
Read it, do all five steps, then come back here for a read.
Please note that you will need to enable hidden APIs for API Level 18(4.3) and one API from 14 to 17. I recommend doing 17.

So, you've enabled hidden and internal API, hacked your ADT plugin, and you're craving for knowledge? Good.

Now some theory.
When the metadata is sent by RemoteControlClient, it is consumed by object called RemoteControlDisplay.
But the problem is, there's no explicit RemoteControlDisplay class, but there is only AIDL interface called IRemoteControlDisplay.



2. Understanding IRemoteControlDisplay

So, let's check which methods this interface has.

void setCurrentClientId(int clientGeneration, in PendingIntent clientMediaIntent, boolean clearing);
This method is used to connect music player to your RemoteControlDisplay.
First parameter is an internal ID of current player.
Second parameter is PendingIntent which will be used for controlling the playback - this is the "address" where you will send commands like "stop playback", "switch to next", etc.
About third parameter...my guess is that it's used when the RemoteControlDisplay is disconnected from current music player. You don't really ned this one.

For next methods I will explain only useful parameters.

void setPlaybackState(int generationId, int state, long stateChangeTimeMs);
This method is called when playback state has changed. For example, it's called when you pause your music.
"state" is obviously the current state of your music player.
It can be one of the following values:

Rarely used:
RemoteControlClient.PLAYSTATE_ERROR - well, there was some kind of error. Normally, you won't get this one.
RemoteControlClient.PLAYSTATE_BUFFERING - the music is buffering and will start playing very-very soon.

Normally used:
RemoteControlClient.PLAYSTATE_PAUSED - the music is paused
RemoteControlClient.PLAYSTATE_PLAYING - the music is playing.

You can check other PLAYSTATE_ constant in RemoteControlClient API reference.

void setTransportControlFlags(int generationId, int transportControlFlags);
In lockscreen it is used for toggling the widget visibility. I couldn't find any appliance for this method in my apps. Well, it sets flags :D

void setMetadata(int generationId, in Bundle metadata);
Well, that's obvious. It is called when RemoteControlDisplay have to update current track metadata.
The Bundle which we are receiving containing some metadata.
The keys for them are all in class MediaMetadataRetriever.
So, for example, to extract song title, you have to do it this way:
Code:
String title=metadata.getString(Integer.toString(MediaMetadataRetriever.METADATA_KEY_TITLE));
From my research I've found that this Bundle can have the following entries:

Those are for "String" entries:
MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST
MediaMetadataRetriever.METADATA_KEY_ARTIST
MediaMetadataRetriever.METADATA_KEY_ALBUM
MediaMetadataRetriever.METADATA_KEY_TITLE

And this one is "long":
MediaMetadataRetriever.METADATA_KEY_DURATION

void setArtwork(int generationId, in Bitmap artwork);
This one is way too obvious. It gives you the Bitmap with artwork of current song. If there is no artwork, the "artwork" parameter will be null.

void setAllMetadata(int generationId, in Bundle metadata, in Bitmap artwork);
This call just combines previous two.

3. Implementing IRemoteControlDisplay

Hey, I now know everything about RemoteControlDisplay, I will implement my own in split second.
Code:
public class MyRemoteControlDisplay implements IRemoteControlDisplay
Please note that IT WON'T WORK THIS WAY!
As IRemoteControlDisplay is actually a AIDL interface, we need to somehow handle marshalling and unmarshalling of data. But luckily, we don't need to think about it. There is a class which handles basic IPC operations - IRemoteControlDisplay$Stub. We just need to extend it.

So, the right way to implement your own RemoteControlDisplayClass is:
Code:
public class MyRemoteControlDisplay extends IRemoteControlDisplay.Stub

Then you will have to implement methods of IRemoteControlDisplay. However, now listen to me carefully. Please, don't try to write your own super-cool implementation.

Just copy and paste the following code
Code:
public MyRemoteControlDisplay extends IRemoteControlDisplay.Stub {

static final int MSG_SET_ARTWORK = 104;
static final int MSG_SET_GENERATION_ID = 103;
static final int MSG_SET_METADATA = 101;
static final int MSG_SET_TRANSPORT_CONTROLS = 102;
static final int MSG_UPDATE_STATE = 100;

private WeakReference<Handler> mLocalHandler;

MyRemoteControlDisplay(Handler handler) {
	mLocalHandler = new WeakReference<Handler>(handler);
}

public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
			Handler handler = mLocalHandler.get();
			if (handler != null) {
				handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
				handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
			}
		}

		public void setArtwork(int generationId, Bitmap bitmap) {
			Handler handler = mLocalHandler.get();
			if (handler != null) {
				handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
			}
		}

		public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent,
				boolean clearing) throws RemoteException {
			Handler handler = mLocalHandler.get();
			if (handler != null) {
				handler.obtainMessage(MSG_SET_GENERATION_ID, clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget();
			}
		}

		public void setMetadata(int generationId, Bundle metadata) {
			Handler handler = mLocalHandler.get();
			if (handler != null) {
				handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
			}
		}

		public void setPlaybackState(int generationId, int state, long stateChangeTimeMs) {
			Handler handler = mLocalHandler.get();
			if (handler != null) {
				handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
			}
		}
		
		public void setTransportControlFlags(int generationId, int flags) {
			Handler handler = mLocalHandler.get();
			if (handler != null) {
				handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags).sendToTarget();
			}
		}

	}

Why we have to implement it this way?
Well, it's because those methods calls arrive asynchronously, so, to correctly process them all, we need a Handler. Then we send messages to this Handler with necessary parameters, and it processes them.

But why use this weird WeakReference? Well, I can't explain it better than Google Developers. Citing the source code comment:
/**
* This class is required to have weak linkage
* because the remote process can hold a strong reference to this binder object and
* we can't predict when it will be GC'd in the remote process. Without this code, it
* would allow a heavyweight object to be held on this side of the binder when there's
* no requirement to run a GC on the other side.
*/
Tl;dr it's just a clever usage of memory resources.

So, my congratulations! We've implemented hidden IRemoteControlDisplay interface. But now it doesn't actually do anything.

4. Using our RCD implementation
As you can see, the constructor requires a Handler to be passed to it.
But any Handler just won't do, as we have to process messages.

So, you can't just write
Code:
MyRemoteControlDisplay display=new MyRemoteControlDisplay(new Handler());

Then, let's implement our Handler.
Again, I recommend you to use the following code, as it's doing it's job fine. This is the code used by default lockscreen with slight modifications. You can try and implement your own Handler, but this is strongly discouraged, as it can get glitchy. Don't worry, we'll soon get to the part where you can use your imagination ;)
Code:
private int mClientGeneration;
private PendingIntent mClientIntent;
private Bitmap mArtwork;
public static final int MSG_SET_ARTWORK = 104;
public static final int MSG_SET_GENERATION_ID = 103;
public static final int MSG_SET_METADATA = 101;
public static final int MSG_SET_TRANSPORT_CONTROLS = 102;
public static final int MSG_UPDATE_STATE = 100;

Handler myHandler = new Handler(new Handler.Callback() {
		            [user=439709]@override[/user]
			public boolean handleMessage(Message msg) {
					switch (msg.what) {
					case MSG_UPDATE_STATE:
                                                //if client generation is correct(our client is still active), we do some stuff to indicate change of playstate
						if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2);
						break;

                                                //if client generation is correct(our client is still active), we do some stuff to update our metadata
					case MSG_SET_METADATA:
						if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj);
						break;

					case MSG_SET_TRANSPORT_CONTROLS:
						break;

                                            //if our client has changed, we update the PendingIntent to control it and save new generation id
					case MSG_SET_GENERATION_ID:
						mClientGeneration = msg.arg1;
						mClientIntent = (PendingIntent) msg.obj;
						break;

                                                //if client generation is correct(our client is still active), we do some stuff to update our artwork
                                                //while recycling the old one to reduce memory usage
					case MSG_SET_ARTWORK:
						if (mClientGeneration == msg.arg1) {
							if (mArtwork != null) {
								mArtwork.recycle();
							}
							mArtwork = (Bitmap)msg.obj;
							setArtwork(mArtwork);
						}
						break;
					}
				return true;
			}
		});

I think you can probably guess what do we do here, but I'll explain anyway.

updatePlayPauseState(msg.arg2) - it's the method where you actually handle the change of playstate.
msg.arg2 is an Integer(or, more correcly, int), which indicates current play state. Please refer to section 2 to see possible play states. For example, you may do check like this:
Code:
updatePlayState(int state) {
           if(state==RemoteControlClient.PLAYSTATE_PLAYING) {
                     setButtonImage(R.drawable.play);
               } else {
                     setButtonImage(R.drawable.pause);
               }
}

setArtwork(mArtwork);
It's pretty obvious, do whatever you like with this bitmap, just remember that it can be null.

updateMetadata((Bundle) msg.obj)
That one isn't very easy to handle. There is a bundle containing all of available metadata. So, you can deal with it as you please(you know how to extract data from Bundle, right?), but here's how I do it(modified Google version):
Code:
private void updateMetadata(Bundle data) {
		String artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
		//if we failed to get artist, then we should try another field
		if(artist==null) {
			artist=getMdString(data, MediaMetadataRetriever.METADATA_KEY_ARTIST);
		}
		if(artist==null) {
                        //if we still failed to get artist, we will return a string containing "Unknown"
			artist=mContext.getResources().getString(R.string.unknown);
		}
                //same idea for title
		String title = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE);
		if(title==null) {
			title=mContext.getResources().getString(R.string.unknown);
		}
		//album isn't that necessary, so I just ignore it if it's null
		String album=getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM);
		if((artist!=null)&&(title!=null)) {
			setMetadata(artist, title, album);
		}
	}

	private void updatePlayPauseState(int state) {
		mPlayButtonState=state;
		mService.setPlayButtonState(state);
	}

       public void setMetadata(String artist, String title, String album) {
		mMetadataArtistTextView.setText(artist);
		mMetadataTitleTextView.setText(title);
		if(album!=null) {
			mMetadataAlbumTextView.setText(album);
		} else {
			mMetadataAlbumTextView.setText("");
		}
	}
}

private String getMdString(Bundle data, int id) {
		return data.getString(Integer.toString(id));
	}

You've got the idea.

Okay. So you've implemented your Handler, and created MyRemoteControlDisplayObject. What's next?

5. Registering and unregistering RemoteControlDisplay

In order for your RemoteControlDisplay to be able to receive metadata and album arts, it has to be registered via AudioManager.
You have to get the instance of AudioManager and then call Audiomanager#registerRemoteControlDisplay().

Code:
MyHandler myHandler=new MyHandler();
MyRemoteControlDisplay remoteDisplay=new MyRemoteControlDisplay(myHandler);
	AudioManager manager=((AudioManager)mContext.getSystemService("audio"));
	mAudioManager.registerRemoteControlDisplay(remoteDisplay);

So, that's it. You've succesfully registered your display. Now it will receive metadata and album art.

However, that's not all.
When the metadata isn't visible to user, you should unregister your RemoteControlDisplay by using this code:
Code:
audioManager.unregisterRemoteControlDisplay(remoteDisplay);
remoteDisplay=null;
mHandler.removeMessages(MSG_SET_GENERATION_ID);
mHandler.removeMessages(MSG_SET_METADATA);
mHandler.removeMessages(MSG_SET_TRANSPORT_CONTROLS);
mHandler.removeMessages(MSG_UPDATE_STATE);

This way you will unregister your RemoteControlDisplay, destroy it(or, actually, just give it garbage collector), and remove all unprocessed messages. This is the correct way to unregister your RemoteControlDisplay.

Please note! You must register your RemoteControlDisplay every time when the View which displays metadata is shown to the user. This is because 4.2.2 and lower versions support only one RemoteControlDisplay, and if system will decide to register it's own RCD, your RCD will be unregistered automatically.

When you're compiling, you have to compile with 4.2.2 modified library for 4.2.2 and lower, and compile with 4.3 modified library for 4.3. That is very important, because if you'll try to launch 4.2.2 implementation on device running 4.3, it will give you AbstractMethodError.

If you have any question regarding the implementation, please ask here. Don't ask "How do I start Eclipse", or anything like that.

And please, if you use this, at least give me a credit. Finding this out was a really hard job.

To understand how it works, I've used source code from Android GitHub repository:
KeyguardTransportControlView.java

Or donate to me, whatever you like more. However, it would be the best if you give me credit and donate to me ;)
 
Last edited:

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
Reserved for Android 4.3 implementation.

So, in 4.3 Google change some IRemoteControlDisplay methods.

1. Implementing IRemoteControlDisplay

Those are:
void setTransportControlFlags(int generationId, int transportControlFlags); is replaced by
void setTransportControlInfo(int generationId, int transportControlFlags, int posCapabilities)

The new parameter - posCapabilities - is indicating whether the RemoteControlClient provides information about playback position. It is a bit mask for those (public, but hidden) constants:
0 - no info about playback position
RemoteControlClient.MEDIA_POSITION_WRITABLE - playback position can be changed
RemoteControlClient.MEDIA_POSITION_READABLE - playback position can be read

void setPlaybackState(int generationId, int state, long stateChangeTimeMs); is replaced by
void setPlaybackState(int generationId, int state, long stateChangeTimeMs, long currentPosMs,float speed);

New parameters:
currentPosMs - current playback position in milliseconds.
If it's positive, it's the current playback position.
Negative values means that the position is unknown.
RemoteControlClient.PLAYBACK_POSITION_INVALID means that position is unknown, like if you're listening to live radio.
RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN - that means the music player doesn't provide current position at all, because it's using legacy API(from 4.2.2).

speed - is the playback speed. 1.0 is normal, 0.5 is slower, 2.0 is 2x faster.

The rest stays the same.
However, the IRemoteControlDisplay implementation in my first post doesn't send this kind of data. So, if you want to send this data to your Handler, you may want to pack it into Bundle and send it as Message.obj.

2. Registering RemoteControlDisplay


Now there are two methods:
registerRemoteControlDisplay(IRemoteControlDisplay rcd)
RemoteControlDisplay registered with this method will NOT receive any artwork bitmaps.
Use this method only if you DO NOT NEED ARTWORK.

registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h)
w and h are maximum width and height of expected artwork bitmap. Use this method if you NEED ARTWORK.

3. Multiple RemoteControlDisplays

(FOLLOWING TEXT MAY BE NOT WORKING, AS THIS IS ONLY A THEORY BASED FROM STUDYING OF ANDROID SOURCE CODE)
Until API 18 there could be only one active RemoteControlDisplay. So, that means if you register your RCD, and then system decides to do the same, then your RemoteControlDisplay will be unregistered automatically.

However, this is not the case in 4.3, as from API 18 it does support multiple RemoteControlDisplay. So, in theory, you can just fire AudioManager#registerRemoteControlDisplay. Didn't tried that, however.
And, of course, you have to compile with Android 4.3 library.
 
Last edited:

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
Really cool guide. :good:

Thank you. :)

Thanks! I just hope it will have the desired effect and Google will release RemoteControlDisplay API to public, 'cause, you know, now I made it public, there's no reason to hide it now. Now I only need it to be featured on the main page...

How about making it sticky or something?
 
Last edited:
  • Like
Reactions: SimplicityApks

nikwen

Senior Member
Feb 1, 2013
3,142
1,597
Berlin, Germany
www.nikwen.de
Thanks! I just hope it will have the desired effect and Google will release RemoteControlDisplay API to public, 'cause, you know, now I made it public, there's no reason to hide it now. Now I only need it to be featured on the main page...

How about making it sticky or something?

I voted for it. ;)

The sticky: Contact a mod and if it is relevant to the majority of the users, it will be made sticky.
Those are the mods for the Java development forum: diestarbucks, poyensa, justmpm, mark manning, crachel, Archer
 

vikraminside

Senior Member
Jul 9, 2013
737
199
I was looking for this undocumented api's and customized transparent lock screen that can listen for events.

Thank you. I will close my other open thread now.

God is great.
 

MohammadAG

Inactive Recognized Developer
Sep 7, 2009
1,080
5,504
30
Jerusalem
mohammadag.xceleo.org
@Dr.Alexander_Breen thanks a lot for this, almost spent the day trying to get this to work with reflection.

Anyway, I still can't get it to work.
I'm trying to do this for my S-View cover mod http://xdaforums.com/showthread.php?t=2368665
But the methods aren't called at all.

I've never used handlers before, so I'm guessing there's a problem with those.
Can you take a look at the source code? Since it's an Xposed module, and you might not know Xposed, I'll state a few things about it
https://github.com/MohammadAG/Xpose...ewpowerampmetadata/SViewPowerampMetadata.java

The mod is not a subclass of Activity, so there's no Context, but I do get the Context used by the S-View widget.
After getting the Context, and the Handler used by the S-View classes, I register the RemoteControlDisplay (method initializeRemoteControlDisplay(Looper))
The rest is mostly your code.

If you're wondering why I construct the Handler in such a weird way, it's because I can't create one on the Xposed module thread (something about Looper.prepare()).

Anyway, any help would be appreciated, if the code seems fine, I guess I'll have to use a service and make the module communicate with that instead, though I can't imagine media buttons being any slower.
 

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
@MohammadAG:
Your code looks fine to me, but there is one thing.
You register your RemoteControlDisplay in handleLoadPackage() method. I suppose that this method is called only once, but RemoteControlDisplay needs to be registered every time the metadata view is show to user.

Also, added 4.3 implementation.
 
Last edited:

MohammadAG

Inactive Recognized Developer
Sep 7, 2009
1,080
5,504
30
Jerusalem
mohammadag.xceleo.org
@MohammadAG:
Your code looks fine to me, but there is one thing.
You register your RemoteControlDisplay in handleLoadPackage() method. I suppose that this method is called only once, but RemoteControlDisplay needs to be registered every time the metadata view is show to user.

Also, added 4.3 implementation.

But that's only if I unregister it right? I'll see about it, maybe I need to do the Handler bit different in Xposed.

Thanks :)

Sent from my GT-I9500 using xda app-developers app
 

MohammadAG

Inactive Recognized Developer
Sep 7, 2009
1,080
5,504
30
Jerusalem
mohammadag.xceleo.org
Actually, no. As in 4.2 and lower, there can be only one active RCD. So, in case your system decides to register it's own RCD, it automatically unregisters yours.

Oh, so that explains why your app displayed Unknown at some point. I guess that was my RCD being registered.

Sent from my GT-I9500 using xda app-developers app
 

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
No, actually I haven't :/
I register the display each time the S View screen shows, which is when I want to show metadata, but that doesn't work, the handler's handleMessage method is never called.

Hmm. I'm afraid this is something with Handler in Xposed. You can, however, move IRemoteControlDisplay implementation to service, which will connect with your Xposed part via AIDL or smth like that.

Also, check, if methods of IRemoteControlDisplay are being called. Like, fire a log message when method is called.
 

MohammadAG

Inactive Recognized Developer
Sep 7, 2009
1,080
5,504
30
Jerusalem
mohammadag.xceleo.org
Hmm. I'm afraid this is something with Handler in Xposed. You can, however, move IRemoteControlDisplay implementation to service, which will connect with your Xposed part via AIDL or smth like that.

Also, check, if methods of IRemoteControlDisplay are being called. Like, fire a log message when method is called.

I thought about that, but I'm pretty sure it'll use up more memory and introduce a bit of lag (if the service was killed for example).

I was thinking of hooking registerRemoteControlClient and keeping the registered remote(s). Then I can simply get the data from their methods, do you know if they're kept in memory for as long as the app lives?

Sent from my GT-I9500 using xda app-developers app
 

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
I thought about that, but I'm pretty sure it'll use up more memory and introduce a bit of lag (if the service was killed for example).

I was thinking of hooking registerRemoteControlClient and keeping the registered remote(s). Then I can simply get the data from their methods, do you know if they're kept in memory for as long as the app lives?

Sent from my GT-I9500 using xda app-developers app

Again, please check, if methods of RemoteControlDisplay are being called. Write message to debug log(Log.d) in setMetadata or setAllMetadata methods.

As a workaround, I have this idea.
If I understand correctly, Xposed can hook the method of the class, not the instance. Then you can hook RemoteControlClient methods to get the metadata directly from the clients. Check the API reference to RCC to get necessary methods.
 
Last edited:

sak-venom1997

Senior Member
Feb 4, 2013
928
415
Lucknow
really cool tutorial :)
Thanks a lot !

Could you post one for implimemtation of IBatteryStats...... etc private battery APIs ??

Sent from my GT-S5302 using Tapatalk 2
 

Dr.Alexander_Breen

Senior Member
Jun 17, 2012
439
1,072
really cool tutorial :)
Thanks a lot !

Could you post one for implimemtation of IBatteryStats...... etc private battery APIs ??

Sent from my GT-S5302 using Tapatalk 2

Well, I think the answer is "no", because I'm no Google Software Engineer. I'll look into this, however, as it will be simplier, I think.
Something like connecting remote service with IBatteryStats.Stub.asInterface. I'll look into it, however.
 

sak-venom1997

Senior Member
Feb 4, 2013
928
415
Lucknow
Well, I think the answer is "no", because I'm no Google Software Engineer. I'll look into this, however, as it will be simplier, I think.
Something like connecting remote service with IBatteryStats.Stub.asInterface. I'll look into it, however.

Connecting to serivice and obtaining data i could manage by diging into source but parsing the info is causing trouble

thanks :)

Sent from my GT-S5302 using Tapatalk 2
 

Top Liked Posts

  • There are no posts matching your filters.
  • 13
    Beginner developers, beware! This guide isn't beginner-friendly at all and it's targeted at developers who have some knowledge about Android development.

    Or you can use my new library - Remote Metadata Provider, it's MUCH simplier to use.

    0. The Introduction
    You guys probably seen my apps - Floating Music Widget and Android Control Center.

    They both share one feature - good music player integration. They can show you metadata and Floating Music Widget even shows album art. While some players provide API for external music controls(like PowerAmp), the others just somehow integrate with lockscreen. How? Sit down, get a cup of tea, and listen to me.

    With the API Level 14 Google introduced class called RemoteControlClient. Citing Google API Reference:
    RemoteControlClient enables exposing information meant to be consumed by remote controls capable of displaying metadata, artwork and media transport control buttons.

    I won't explain how this works - you may go and read some tutorials around the web, there are plenty of them.
    Or check API Reference here.

    But. Well, we send metadata and album art. Oh, and on 4.3 we can even send playback position. However...how do we receive it? Well, by some reason, I don't know exactly why, Google has hidden this part of API. Maybe they think it's unsere to let you consume other app data, or maybe they just forgot about it. I've asked them multiple times, why did they hid this part of API, but they just ignored me.
    So, by posting this article, I hope to maybe somehow make them change their minds and publish this API after all.


    1. Getting started
    Please note that this guide won't give you Activity examples, or any other things. It will give you the bare bones of the implementation of your own media controls. It's NOT intended to be used by Android/Java newbies.

    PLEASE NOTE THAT IT'S A CLOSED API! IT MAY MALFUNCTION OR NOT WORK AT ALL!


    Of course, you will need Eclipse IDE.
    Also you will need modified Android build platform with hidden and internal API enabled.
    There's an excellent guide on how to do this:
    Using internal (com.android.internal) and hidden (@hide) APIs
    Read it, do all five steps, then come back here for a read.
    Please note that you will need to enable hidden APIs for API Level 18(4.3) and one API from 14 to 17. I recommend doing 17.

    So, you've enabled hidden and internal API, hacked your ADT plugin, and you're craving for knowledge? Good.

    Now some theory.
    When the metadata is sent by RemoteControlClient, it is consumed by object called RemoteControlDisplay.
    But the problem is, there's no explicit RemoteControlDisplay class, but there is only AIDL interface called IRemoteControlDisplay.



    2. Understanding IRemoteControlDisplay

    So, let's check which methods this interface has.

    void setCurrentClientId(int clientGeneration, in PendingIntent clientMediaIntent, boolean clearing);
    This method is used to connect music player to your RemoteControlDisplay.
    First parameter is an internal ID of current player.
    Second parameter is PendingIntent which will be used for controlling the playback - this is the "address" where you will send commands like "stop playback", "switch to next", etc.
    About third parameter...my guess is that it's used when the RemoteControlDisplay is disconnected from current music player. You don't really ned this one.

    For next methods I will explain only useful parameters.

    void setPlaybackState(int generationId, int state, long stateChangeTimeMs);
    This method is called when playback state has changed. For example, it's called when you pause your music.
    "state" is obviously the current state of your music player.
    It can be one of the following values:

    Rarely used:
    RemoteControlClient.PLAYSTATE_ERROR - well, there was some kind of error. Normally, you won't get this one.
    RemoteControlClient.PLAYSTATE_BUFFERING - the music is buffering and will start playing very-very soon.

    Normally used:
    RemoteControlClient.PLAYSTATE_PAUSED - the music is paused
    RemoteControlClient.PLAYSTATE_PLAYING - the music is playing.

    You can check other PLAYSTATE_ constant in RemoteControlClient API reference.

    void setTransportControlFlags(int generationId, int transportControlFlags);
    In lockscreen it is used for toggling the widget visibility. I couldn't find any appliance for this method in my apps. Well, it sets flags :D

    void setMetadata(int generationId, in Bundle metadata);
    Well, that's obvious. It is called when RemoteControlDisplay have to update current track metadata.
    The Bundle which we are receiving containing some metadata.
    The keys for them are all in class MediaMetadataRetriever.
    So, for example, to extract song title, you have to do it this way:
    Code:
    String title=metadata.getString(Integer.toString(MediaMetadataRetriever.METADATA_KEY_TITLE));
    From my research I've found that this Bundle can have the following entries:

    Those are for "String" entries:
    MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST
    MediaMetadataRetriever.METADATA_KEY_ARTIST
    MediaMetadataRetriever.METADATA_KEY_ALBUM
    MediaMetadataRetriever.METADATA_KEY_TITLE

    And this one is "long":
    MediaMetadataRetriever.METADATA_KEY_DURATION

    void setArtwork(int generationId, in Bitmap artwork);
    This one is way too obvious. It gives you the Bitmap with artwork of current song. If there is no artwork, the "artwork" parameter will be null.

    void setAllMetadata(int generationId, in Bundle metadata, in Bitmap artwork);
    This call just combines previous two.

    3. Implementing IRemoteControlDisplay

    Hey, I now know everything about RemoteControlDisplay, I will implement my own in split second.
    Code:
    public class MyRemoteControlDisplay implements IRemoteControlDisplay
    Please note that IT WON'T WORK THIS WAY!
    As IRemoteControlDisplay is actually a AIDL interface, we need to somehow handle marshalling and unmarshalling of data. But luckily, we don't need to think about it. There is a class which handles basic IPC operations - IRemoteControlDisplay$Stub. We just need to extend it.

    So, the right way to implement your own RemoteControlDisplayClass is:
    Code:
    public class MyRemoteControlDisplay extends IRemoteControlDisplay.Stub

    Then you will have to implement methods of IRemoteControlDisplay. However, now listen to me carefully. Please, don't try to write your own super-cool implementation.

    Just copy and paste the following code
    Code:
    public MyRemoteControlDisplay extends IRemoteControlDisplay.Stub {
    
    static final int MSG_SET_ARTWORK = 104;
    static final int MSG_SET_GENERATION_ID = 103;
    static final int MSG_SET_METADATA = 101;
    static final int MSG_SET_TRANSPORT_CONTROLS = 102;
    static final int MSG_UPDATE_STATE = 100;
    
    private WeakReference<Handler> mLocalHandler;
    
    MyRemoteControlDisplay(Handler handler) {
    	mLocalHandler = new WeakReference<Handler>(handler);
    }
    
    public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
    			Handler handler = mLocalHandler.get();
    			if (handler != null) {
    				handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
    				handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
    			}
    		}
    
    		public void setArtwork(int generationId, Bitmap bitmap) {
    			Handler handler = mLocalHandler.get();
    			if (handler != null) {
    				handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
    			}
    		}
    
    		public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent,
    				boolean clearing) throws RemoteException {
    			Handler handler = mLocalHandler.get();
    			if (handler != null) {
    				handler.obtainMessage(MSG_SET_GENERATION_ID, clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget();
    			}
    		}
    
    		public void setMetadata(int generationId, Bundle metadata) {
    			Handler handler = mLocalHandler.get();
    			if (handler != null) {
    				handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
    			}
    		}
    
    		public void setPlaybackState(int generationId, int state, long stateChangeTimeMs) {
    			Handler handler = mLocalHandler.get();
    			if (handler != null) {
    				handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
    			}
    		}
    		
    		public void setTransportControlFlags(int generationId, int flags) {
    			Handler handler = mLocalHandler.get();
    			if (handler != null) {
    				handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags).sendToTarget();
    			}
    		}
    
    	}

    Why we have to implement it this way?
    Well, it's because those methods calls arrive asynchronously, so, to correctly process them all, we need a Handler. Then we send messages to this Handler with necessary parameters, and it processes them.

    But why use this weird WeakReference? Well, I can't explain it better than Google Developers. Citing the source code comment:
    /**
    * This class is required to have weak linkage
    * because the remote process can hold a strong reference to this binder object and
    * we can't predict when it will be GC'd in the remote process. Without this code, it
    * would allow a heavyweight object to be held on this side of the binder when there's
    * no requirement to run a GC on the other side.
    */
    Tl;dr it's just a clever usage of memory resources.

    So, my congratulations! We've implemented hidden IRemoteControlDisplay interface. But now it doesn't actually do anything.

    4. Using our RCD implementation
    As you can see, the constructor requires a Handler to be passed to it.
    But any Handler just won't do, as we have to process messages.

    So, you can't just write
    Code:
    MyRemoteControlDisplay display=new MyRemoteControlDisplay(new Handler());

    Then, let's implement our Handler.
    Again, I recommend you to use the following code, as it's doing it's job fine. This is the code used by default lockscreen with slight modifications. You can try and implement your own Handler, but this is strongly discouraged, as it can get glitchy. Don't worry, we'll soon get to the part where you can use your imagination ;)
    Code:
    private int mClientGeneration;
    private PendingIntent mClientIntent;
    private Bitmap mArtwork;
    public static final int MSG_SET_ARTWORK = 104;
    public static final int MSG_SET_GENERATION_ID = 103;
    public static final int MSG_SET_METADATA = 101;
    public static final int MSG_SET_TRANSPORT_CONTROLS = 102;
    public static final int MSG_UPDATE_STATE = 100;
    
    Handler myHandler = new Handler(new Handler.Callback() {
    		            [user=439709]@override[/user]
    			public boolean handleMessage(Message msg) {
    					switch (msg.what) {
    					case MSG_UPDATE_STATE:
                                                    //if client generation is correct(our client is still active), we do some stuff to indicate change of playstate
    						if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2);
    						break;
    
                                                    //if client generation is correct(our client is still active), we do some stuff to update our metadata
    					case MSG_SET_METADATA:
    						if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj);
    						break;
    
    					case MSG_SET_TRANSPORT_CONTROLS:
    						break;
    
                                                //if our client has changed, we update the PendingIntent to control it and save new generation id
    					case MSG_SET_GENERATION_ID:
    						mClientGeneration = msg.arg1;
    						mClientIntent = (PendingIntent) msg.obj;
    						break;
    
                                                    //if client generation is correct(our client is still active), we do some stuff to update our artwork
                                                    //while recycling the old one to reduce memory usage
    					case MSG_SET_ARTWORK:
    						if (mClientGeneration == msg.arg1) {
    							if (mArtwork != null) {
    								mArtwork.recycle();
    							}
    							mArtwork = (Bitmap)msg.obj;
    							setArtwork(mArtwork);
    						}
    						break;
    					}
    				return true;
    			}
    		});

    I think you can probably guess what do we do here, but I'll explain anyway.

    updatePlayPauseState(msg.arg2) - it's the method where you actually handle the change of playstate.
    msg.arg2 is an Integer(or, more correcly, int), which indicates current play state. Please refer to section 2 to see possible play states. For example, you may do check like this:
    Code:
    updatePlayState(int state) {
               if(state==RemoteControlClient.PLAYSTATE_PLAYING) {
                         setButtonImage(R.drawable.play);
                   } else {
                         setButtonImage(R.drawable.pause);
                   }
    }

    setArtwork(mArtwork);
    It's pretty obvious, do whatever you like with this bitmap, just remember that it can be null.

    updateMetadata((Bundle) msg.obj)
    That one isn't very easy to handle. There is a bundle containing all of available metadata. So, you can deal with it as you please(you know how to extract data from Bundle, right?), but here's how I do it(modified Google version):
    Code:
    private void updateMetadata(Bundle data) {
    		String artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
    		//if we failed to get artist, then we should try another field
    		if(artist==null) {
    			artist=getMdString(data, MediaMetadataRetriever.METADATA_KEY_ARTIST);
    		}
    		if(artist==null) {
                            //if we still failed to get artist, we will return a string containing "Unknown"
    			artist=mContext.getResources().getString(R.string.unknown);
    		}
                    //same idea for title
    		String title = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE);
    		if(title==null) {
    			title=mContext.getResources().getString(R.string.unknown);
    		}
    		//album isn't that necessary, so I just ignore it if it's null
    		String album=getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM);
    		if((artist!=null)&&(title!=null)) {
    			setMetadata(artist, title, album);
    		}
    	}
    
    	private void updatePlayPauseState(int state) {
    		mPlayButtonState=state;
    		mService.setPlayButtonState(state);
    	}
    
           public void setMetadata(String artist, String title, String album) {
    		mMetadataArtistTextView.setText(artist);
    		mMetadataTitleTextView.setText(title);
    		if(album!=null) {
    			mMetadataAlbumTextView.setText(album);
    		} else {
    			mMetadataAlbumTextView.setText("");
    		}
    	}
    }
    
    private String getMdString(Bundle data, int id) {
    		return data.getString(Integer.toString(id));
    	}

    You've got the idea.

    Okay. So you've implemented your Handler, and created MyRemoteControlDisplayObject. What's next?

    5. Registering and unregistering RemoteControlDisplay

    In order for your RemoteControlDisplay to be able to receive metadata and album arts, it has to be registered via AudioManager.
    You have to get the instance of AudioManager and then call Audiomanager#registerRemoteControlDisplay().

    Code:
    MyHandler myHandler=new MyHandler();
    MyRemoteControlDisplay remoteDisplay=new MyRemoteControlDisplay(myHandler);
    	AudioManager manager=((AudioManager)mContext.getSystemService("audio"));
    	mAudioManager.registerRemoteControlDisplay(remoteDisplay);

    So, that's it. You've succesfully registered your display. Now it will receive metadata and album art.

    However, that's not all.
    When the metadata isn't visible to user, you should unregister your RemoteControlDisplay by using this code:
    Code:
    audioManager.unregisterRemoteControlDisplay(remoteDisplay);
    remoteDisplay=null;
    mHandler.removeMessages(MSG_SET_GENERATION_ID);
    mHandler.removeMessages(MSG_SET_METADATA);
    mHandler.removeMessages(MSG_SET_TRANSPORT_CONTROLS);
    mHandler.removeMessages(MSG_UPDATE_STATE);

    This way you will unregister your RemoteControlDisplay, destroy it(or, actually, just give it garbage collector), and remove all unprocessed messages. This is the correct way to unregister your RemoteControlDisplay.

    Please note! You must register your RemoteControlDisplay every time when the View which displays metadata is shown to the user. This is because 4.2.2 and lower versions support only one RemoteControlDisplay, and if system will decide to register it's own RCD, your RCD will be unregistered automatically.

    When you're compiling, you have to compile with 4.2.2 modified library for 4.2.2 and lower, and compile with 4.3 modified library for 4.3. That is very important, because if you'll try to launch 4.2.2 implementation on device running 4.3, it will give you AbstractMethodError.

    If you have any question regarding the implementation, please ask here. Don't ask "How do I start Eclipse", or anything like that.

    And please, if you use this, at least give me a credit. Finding this out was a really hard job.

    To understand how it works, I've used source code from Android GitHub repository:
    KeyguardTransportControlView.java

    Or donate to me, whatever you like more. However, it would be the best if you give me credit and donate to me ;)
    5
    Reserved for Android 4.3 implementation.

    So, in 4.3 Google change some IRemoteControlDisplay methods.

    1. Implementing IRemoteControlDisplay

    Those are:
    void setTransportControlFlags(int generationId, int transportControlFlags); is replaced by
    void setTransportControlInfo(int generationId, int transportControlFlags, int posCapabilities)

    The new parameter - posCapabilities - is indicating whether the RemoteControlClient provides information about playback position. It is a bit mask for those (public, but hidden) constants:
    0 - no info about playback position
    RemoteControlClient.MEDIA_POSITION_WRITABLE - playback position can be changed
    RemoteControlClient.MEDIA_POSITION_READABLE - playback position can be read

    void setPlaybackState(int generationId, int state, long stateChangeTimeMs); is replaced by
    void setPlaybackState(int generationId, int state, long stateChangeTimeMs, long currentPosMs,float speed);

    New parameters:
    currentPosMs - current playback position in milliseconds.
    If it's positive, it's the current playback position.
    Negative values means that the position is unknown.
    RemoteControlClient.PLAYBACK_POSITION_INVALID means that position is unknown, like if you're listening to live radio.
    RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN - that means the music player doesn't provide current position at all, because it's using legacy API(from 4.2.2).

    speed - is the playback speed. 1.0 is normal, 0.5 is slower, 2.0 is 2x faster.

    The rest stays the same.
    However, the IRemoteControlDisplay implementation in my first post doesn't send this kind of data. So, if you want to send this data to your Handler, you may want to pack it into Bundle and send it as Message.obj.

    2. Registering RemoteControlDisplay


    Now there are two methods:
    registerRemoteControlDisplay(IRemoteControlDisplay rcd)
    RemoteControlDisplay registered with this method will NOT receive any artwork bitmaps.
    Use this method only if you DO NOT NEED ARTWORK.

    registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h)
    w and h are maximum width and height of expected artwork bitmap. Use this method if you NEED ARTWORK.

    3. Multiple RemoteControlDisplays

    (FOLLOWING TEXT MAY BE NOT WORKING, AS THIS IS ONLY A THEORY BASED FROM STUDYING OF ANDROID SOURCE CODE)
    Until API 18 there could be only one active RemoteControlDisplay. So, that means if you register your RCD, and then system decides to do the same, then your RemoteControlDisplay will be unregistered automatically.

    However, this is not the case in 4.3, as from API 18 it does support multiple RemoteControlDisplay. So, in theory, you can just fire AudioManager#registerRemoteControlDisplay. Didn't tried that, however.
    And, of course, you have to compile with Android 4.3 library.
    2
    @MohammadAG:
    Your code looks fine to me, but there is one thing.
    You register your RemoteControlDisplay in handleLoadPackage() method. I suppose that this method is called only once, but RemoteControlDisplay needs to be registered every time the metadata view is show to user.

    Also, added 4.3 implementation.
    2
    Maybe nobody's interested, but I'm currently working on the library which will hide all the unnecessary complexity from the user.

    It will use some simple syntax, like setOnMetadataChangeListener etc.
    1
    Really cool guide. :good:

    Thank you. :)

    Thanks! I just hope it will have the desired effect and Google will release RemoteControlDisplay API to public, 'cause, you know, now I made it public, there's no reason to hide it now. Now I only need it to be featured on the main page...

    How about making it sticky or something?