Android – Media player and ASP.NET Web Api

Let’s start with a question. When was the last time you played a song offline at your computer? For me, It’s long,loong,looong… time ago. Since the boom of music websites, I just choose one provider, create my favorite playlist and listen to music online. I don’t have any CD or .mp3 song at my local computer anymore, except that there are still a lot of songs in my Ipod but I don’t know where he is right now 😕 and that’s … bad. If something wrong happens with the website, my song will go with the wind. Moreover nowadays, in the dangerous internet, I doubt all of apps (if they are harmful) and consider a lot every time I install an app on my Android smartphone. So I always try to write the app or software myself if they aren’t complex. For this case, for my favorite songs, I decide to write a very simple media player connecting to my web service, playing my own song and I don’t have to worry anymore about the problem mentioned above. That’s is the concept. Next, I would like to show my media player works, if you want to improve it, go on!

Before get started, just one small note, like any my Android app, I use roboguice as dependency container. If you don’t know how it works, how to configure it or even don’t know what it is, please read this article first Android, Dependency Injection (IOC) with roboguice and MVVM (Model-View-ViewModel) pattern.

Back to my media player, the app is really simple, there are only 2 activities, one for playlist and one for media player.

Playlist

Media player

The layout of media player is as same as Windows Media Player. I just took the background of Windows Media Player and use icons from this set here

http://www.iconarchive.com/show/I-like-buttons-icons-by-mazenl77.html

I have never been a good designer so don’t be annoyed with my cheap GUI :), improve the UI to make it more beautiful as you can.

1. Web service

As I described before about how my media player works, there are 2 important components in my app: the web service and the Android client. The web service or server will store all information about my play list such as the songs itself (.mp3), thumbnails of songs (.jpg) and songs metadatas (name, artist, genre… in database). That means there is nothing saved at my Android phone, the Android client will call the web service and stream songs directly from there. So, no internet no music. The cost will be a big problem if you don’t have flat internet package. However just skip the problem of money and analyze how the web service works.

For all requirements from Android client, I need only two controllers at the server: one for readable data and one for binary data. When I say readable data, I mean really human readable data. For example song name, artist, genre. The binary data is what human can’t understand like binary stream of .mp3 file or .jpg file.

1.1 Play list

The readable data are ready to be used under SongsController.

public class SongsController : ApiController
{
	private AndroidEntities androidEntities = new AndroidEntities();

	public IEnumerable<Song> Get()
	{
		return androidEntities.Songs.AsEnumerable().Select(x => new Song() { Id = x.Id, Name = x.Name });
	}

	public Song Get(int id)
	{
		Song song = androidEntities.Songs.Where(x => x.Id == id).FirstOrDefault();
		if (song != null)
			song.FileName = null;
		return song;
	}
}

This controller provides us 2 actions to get readable data from web service. One for getting all songs and one for getting individual song. I use database to store the information, but if you like to keep as simple as possible, you just copy files to server folder, give the list of file name back as playlist. It works too. This information was entered by me manually but it can also be extracted directly from mp3 tags. There’s still a lot of space for improvement, but for a blog post of 1000 words I would like to keep everything simple as it should be.

So the Song entity is simple, it contains 3 properties: Id, Name (displayed name on playlist) and FileName (file name with extension only without path)

Song entity

Like any REST web service, this Songs controller can be simply called by a GET request. For example, let’s call my sample web service from your browser

http://restwebserviceforandroid.apphb.com/api/songs

You will get a list like this

Song list

Is the file name property always null? Is that right, maybe something wrong here? No, that’s correct, file name is always null. I intentionally hide file name from external call. It doesn’t make sense to expose this sensible information to Android client, the client doesn’t need it.

The other action of the controller allows us to get detailed information of each song. If your song entity has more information such as artist, genre…, you can give them back through this action. The URL for this action looks like following example

http://restwebserviceforandroid.apphb.com/api/songs?id=3

1.2 Download song or thumbnail

We already have the readable data part provided by SongsController, we still need the binary part for song itself (.mp3) and thumbnail of album (.jpg). For this kind of data I make another controller called SongController. This controller has only one action accepting 2 parameters : type and id.

As in code listing below, ‘type’ can only be either ‘song’ or ‘thumbnail’. According to value of ‘type’, controller will search in folder at server for file. If file exists, an HttpResponseMessage will be created with content of that file and stream back to client.

public class SongController : ApiController
{
	private AndroidEntities androidEntities = new AndroidEntities();

	public HttpResponseMessage Get(string type, int id)
	{
		HttpResponseMessage result = null;

		Song foundSong = androidEntities.Songs.Where(x => x.Id == id).FirstOrDefault();
		if (foundSong != null)
		{
			string filePath = null;
			if (type.ToLower() == "song")
				filePath = HostingEnvironment.MapPath("~/App_Data/" + foundSong.FileName);
			else if (type.ToLower() == "thumbnail")
				filePath = HostingEnvironment.MapPath("~/App_Data/" + Path.GetFileNameWithoutExtension(foundSong.FileName) + ".jpg");

			if (File.Exists(filePath))
			{
				FileStream fs = new FileStream(filePath, FileMode.Open);

				result = new HttpResponseMessage(HttpStatusCode.OK);
				result.Content = new StreamContent(fs);
				result.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
				result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
				result.Content.Headers.ContentDisposition.FileName = foundSong.Name  + Path.GetExtension(filePath);
			}
			else
			{
				new AppHbLogEvent(filePath).Raise();
				result = new HttpResponseMessage(HttpStatusCode.NotFound);
			}
		}
		else
		{
			result = new HttpResponseMessage(HttpStatusCode.NotFound);
		}
		return result;
	}
}

The correct format of URL has this format

http://restwebserviceforandroid.apphb.com/api/songs?type=song&id=3

When file is not found at server, you will receive a HttpStatusCode.NotFound status code back. So you can use HttpStatusCode for checking the existence of file in server before streaming it to client.

That’s all about our web service, it’s pretty simple.

2. Android client

Now we reach the 2. component of the app: the Android client. As I mentioned above, the client consists of 2 activities: one for song list and one for media player. If you already read my previous posts before, you’ll be familiar with my code style: One activity has one view model. It’s same here for song list activity.

2.1 Play list

In code listing below, the view model has a synchronous function getSongs() which returns a list of available songs on server. Although the function works in synchronous mode, the function can be called asynchronously in activity in a asynchronous task and then popup the result back to list view. You can also recognize that I use dependency injection for initializing the view model by setting keyword @Inject before declaration of variable. To understand how dependency injection works, read the article that I mentioned at the beginning of the post.

public class MainActivity extends RoboListActivity {

    @Inject private IMainActivityViewModel viewModel;
    ArrayList<HashMap<String, String>> songList;
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        new LoadAllSongs().execute();

        ListView lv = getListView();
        lv.setOnItemClickListener(listViewItemClickListener);
    }

    private AdapterView.OnItemClickListener listViewItemClickListener = new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Integer songId = Integer.parseInt(((TextView) view.findViewById(R.id.textViewId)).getText().toString());

            ArrayList<Integer> songListIds = new ArrayList<Integer>();
            for(HashMap<String,String> keyValuePair: songList)
            {
                songListIds.add(Integer.valueOf(keyValuePair.values().toArray()[1].toString()));
            }

            Intent intent = new Intent(getApplicationContext(), MediaPlayerActivity.class);
            Bundle bundle = new Bundle();
            bundle.putInt(Song.SONG_ID,songId);
            bundle.putIntegerArrayList(Song.SONG_IDs, songListIds);
            intent.putExtra(Song.SONG, bundle);
            startActivity(intent);
        }
    };

    class LoadAllSongs extends AsyncTask<String, String,String>
    {
        private ProgressDialog progressDialog;

        @Override
        protected String doInBackground(String... params) {
            songList = viewModel.getSongs();
            return  null;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();    //To change body of overridden methods use File | Settings | File Templates.
            progressDialog = new ProgressDialog(MainActivity.this);
            progressDialog.setMessage("Loading songs. Please wait...");
            progressDialog.show();
        }

        @Override
        protected void onPostExecute(String s) {
            progressDialog.dismiss();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    ListAdapter adapter = new SimpleAdapter(MainActivity.this, songList, R.layout.list_item, new String[]{"Id","Name"}, new int[]{R.id.textViewId, R.id.textViewName} );
                    setListAdapter(adapter);
                }
            });
            super.onPostExecute(s);    //To change body of overridden methods use File | Settings | File Templates.
        }
    }

}

The play list activity is a subclass of list activity. To get current adapted list view, let’s call getListView(). The list view will display names of all songs and should play the song when user clicks on the item. So I have to handle OnItemClickListener() event, call MediaPlayerActivity, give him correct Extra value and song will be streamed in Media player.

2.2 Media player

The media player activity is little more complex because he has more buttons for controlling the playback. Like play list activity, I also use dependency injection at this activity. All buttons will be just simply initialized by inserting @InjectView keyword before the variables. You all know how these button work therefore I won’t explain their functionality again. I just shortly explain how the code works.

public class MediaPlayerActivity extends RoboActivity {

    ...

    @InjectView(R.id.textViewTitle) private TextView textViewTitle;
    @InjectView(R.id.imageButtonPlayList) private ImageButton imageButtonPlayList;
    @InjectView(R.id.imageViewSong) private ImageView imageViewSong;
    @InjectView(R.id.seekBarTimer) private SeekBar seekBarTimer;
    @InjectView(R.id.imageButtonPlay) private ImageButton imageButtonPlay;
    @InjectView(R.id.imageButtonStop) private ImageButton imageButtonStop;
    @InjectView(R.id.imageButtonPrevious) private ImageButton imageButtonPrevious;
    @InjectView(R.id.imageButtonNext) private ImageButton imageButtonNext;
    @InjectView(R.id.imageButtonVolume) private ImageButton imageButtonVolume;

    ...
}

2.2.1 Title and thumbnail of album

The title and thumbnail of album will be set after the song was successfully loaded into media player control and the information of song was successfully loaded from the web service. The thumbnail will be loaded directly from URL to ImageView.

class LoadSingleSong extends AsyncTask<String, String, String> {
	private ProgressDialog progressDialog;
	private Song song=null;
	private Bitmap bitmap =null;
	@Override
	protected void onPreExecute() {
		super.onPreExecute();    //To change body of overridden methods use File | Settings | File Templates.
		progressDialog = new ProgressDialog(MediaPlayerActivity.this);
		progressDialog.setMessage("Loading song details. Please wait...");
		progressDialog.show();

	}

	@Override
	protected String doInBackground(String... params) {
		List<NameValuePair> args = new ArrayList<NameValuePair>();
		args.add(new BasicNameValuePair(Song.SONG_ID, String.valueOf(songId)));
		JSONHttpClient jsonHttpClient = new JSONHttpClient();
		song = jsonHttpClient.Get(ServiceUrl.SONGS, args, Song.class);

		try {
			String urlThumbnail =ServiceUrl.DOWNLOAD_THUMBNAIL+String.valueOf(songId);
			InputStream inputStream = new URL(urlThumbnail).openStream();
			bitmap = BitmapFactory.decodeStream(inputStream);
		} catch (MalformedURLException e) {
			 e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
		} catch (IOException e) {
			e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
		}
		return null;  //To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	protected void onPostExecute(String s) {
		if (song != null) {
			textViewTitle.setText(song.getName());
			imageViewSong.setImageBitmap(bitmap);
		}
		progressDialog.dismiss();
	}
}

2.2.2 Play list

The play list button is the most simple button. What I need to do is stopping the media player and going back the song list activity.

private View.OnClickListener imageButtonPlayListOnClickListener = new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		handler.removeCallbacks(updateSeekBarTask);
		mediaPlayer.release();
		Intent intent = new Intent(getApplicationContext(), MainActivity.class);
		startActivity(intent);
	}
};

2.2.3 Seek bar

The seek bar is a simple indicator for current position of playback. Be careful with Runnable task for updating the seek bar, remember to remove it when media player is not in playing status.

private SeekBar.OnSeekBarChangeListener seekBarTimerOnSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
	@Override
	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
		//To change body of implemented methods use File | Settings | File Templates.
	}

	@Override
	public void onStartTrackingTouch(SeekBar seekBar) {
		handler.removeCallbacks(updateSeekBarTask);
	}

	@Override
	public void onStopTrackingTouch(SeekBar seekBar) {
		handler.removeCallbacks(updateSeekBarTask);
		int songDuration = mediaPlayer.getDuration();
		int currentPosition = (int)((((double)seekBar.getProgress())/100)*songDuration);
		mediaPlayer.pause();
		mediaPlayer.seekTo(currentPosition);
		mediaPlayer.start();
		handler.postDelayed(updateSeekBarTask,100);
	}
};

private Runnable updateSeekBarTask = new Runnable() {
	@Override
	public void run() {
		int songDuration= mediaPlayer.getDuration();
		int currentPosition =  mediaPlayer.getCurrentPosition();
		int progress = (int)((((double)currentPosition)/songDuration)*100);
		seekBarTimer.setProgress(progress);
		handler.postDelayed(this,100);
	}
};

2.2.4 Stop button

Remember to remove call back for updating seek bar and then stop the media player.

private View.OnClickListener imageButtonStopOnClickListener = new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		handler.removeCallbacks(updateSeekBarTask);
		seekBarTimer.setProgress(0);
		imageButtonPlay.setImageResource(R.drawable.play);
		mediaPlayer.stop();
		isStopped = true;
	}
};

2.2.5 Previous and next button

Just imagine the play list as a ring list, I append the last item to the first item, then just jump next or jump back on that list and play song.

private View.OnClickListener imageButonPreviousOnClickListener = new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		for (int index=songIds.size()-1;index>=0;index--)
		{
			if (songIds.get(index) == songId)
			{
				if (index== 0)
					songId=songIds.get(songIds.size()-1);
				else
					songId=songIds.get(index-1);
				break;
			}

		}
		PlaySong();
	}
};
private View.OnClickListener imageButtonNextOnClickListener = new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		PlayNextSong();
	}
};

private void PlayNextSong()
{
	for (int index=0; index < songIds.size();index++)
	{
		if (songIds.get(index) == songId)
		{
			if (index== songIds.size()-1)
				songId=songIds.get(0);
			else
				songId=songIds.get(index+1);
			break;
		}

	}
	PlaySong();
}
private void PlaySong() {
	new LoadSingleSong().execute();
	String urlSong = ServiceUrl.DOWNLOAD_SONG +String.valueOf(songId);


	try {
		mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
		mediaPlayer.reset();
		mediaPlayer.setDataSource(urlSong);
		mediaPlayer.prepare();
		mediaPlayer.start();
		seekBarTimer.setProgress(0);
		seekBarTimer.setMax(100);
		handler.postDelayed(updateSeekBarTask, 100);

	} catch (IOException e) {
		e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
	}
}

2.2.6 Play button

I check if media player is currently playing. If yes, then stop, replace the image of button from ‘stop’ to ‘play’. If no, then play and replace the image of button from ‘play’ to ‘stop’

private View.OnClickListener imageButtonPlayOnClickListener = new View.OnClickListener() {
	@Override
	public void onClick(View v) {

		if (mediaPlayer!= null && mediaPlayer.isPlaying())
		{
			mediaPlayer.pause();
			imageButtonPlay.setImageResource(R.drawable.play);
		}
		else if (mediaPlayer!= null && !mediaPlayer.isPlaying())
		{
			if (!isStopped)
				mediaPlayer.start();
			else
			{
				isStopped = false;
				PlaySong();
			}
			imageButtonPlay.setImageResource(R.drawable.pause);
		}
	}
};

2.2.7 Volume button

For simplicity, I just show the system volume control dialog of Android.

private View.OnClickListener imageButtonVolumeOnClickListener = new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
		audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI);

	}
};

2.3 User interface

The UI is built up by only using xml code. Setting background of image button to @null will give us a transparent background for all buttons and the frames around the buttons will vanish.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent" android:background="@drawable/background_wmp">

    <LinearLayout
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="60dp" android:paddingLeft="5dp" android:paddingRight="5dp" android:layout_alignParentTop="true">
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Song title"
                android:id="@+id/textViewTitle" android:layout_gravity="center_horizontal|top" android:layout_weight="1"
                android:textColor="#ffffff" android:textSize="17dp" android:paddingLeft="10dp"
                android:paddingTop="20dp" android:textStyle="bold"/>
        <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="fill_parent"
                android:id="@+id/imageButtonPlayList" android:layout_gravity="center_horizontal|top"
                android:src="@drawable/list" android:background="@null"/>
    </LinearLayout>

    <ImageView
            android:layout_width="120px"
            android:layout_height="120px"
            android:id="@+id/imageViewSong" android:layout_gravity="center"/>
    <SeekBar android:id="@+id/seekBarTimer" android:layout_width="fill_parent" android:layout_marginTop="20dp" android:layout_marginBottom="20dp"
             android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_height="30dp"></SeekBar>



        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical" android:layout_marginLeft="10dp" android:layout_marginRight="5dp">
            <ImageButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/imageButtonStop" android:src="@drawable/stop" android:background="@null"
                    android:layout_marginRight="30dp"/>
            <ImageButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/imageButtonPrevious" android:src="@drawable/rewind" android:background="@null"/>
            <ImageButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/imageButtonPlay" android:src="@drawable/play" android:background="@null"/>
            <ImageButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/imageButtonNext" android:src="@drawable/forward" android:background="@null"/>
            <ImageButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/imageButtonVolume" android:src="@drawable/speaker"
                    android:background="@null" android:layout_marginLeft="10dp" android:layout_marginTop="1dp"/>
        </LinearLayout>

</LinearLayout>

Now we have a simple media player to use with our custom web service. You can download source code from this link MediaPlayer and ASP.NET Web Api. Let’s improve it. I hope this post will help you to understand more how to apply dependency injection in your project, play media online or show image directly from URL.

2 thoughts on “Android – Media player and ASP.NET Web Api”

  1. First, congratulating for the content of the post, it helped me a lot in doubt as to the mediaplayer on android. But need to take a doubt about your application, when running on an actual device, to close the screen without algumem exit the app or open some other, stops running and does not reset the stop point, and clicking a button gives exception. How to fix this?

    sorry for my English 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *