So far we have concentrated on building applications that are centered around being in the foreground and have their user interface in front of the user. In the last chapter, we looked at how to add audio playback capabilities to those types of applications.
What happens, though, if we want to build an application that plays music or audio books, but we would like the user to be able to do other things with the phone while continuing to listen? We might have some trouble making that happen if we limit ourselves to just building activities. The Android operating system reserves the right to kill activities that aren't in the front and in use by the user. It does this in order to free up memory to make room for other applications to run. If the OS kills an activity that is playing audio, this would stop the audio from playing, making the user experience not so great.
Fortunately, there is a solution. Instead of playing our audio in an activity, we can use a Service.
In order to ensure that the audio continues to play when the application is no longer in the front and its activity is not in use, we need to create a Service. A Service is a component of an Android application that is meant to run tasks in the background without requiring any interaction from the user.
There are a couple of different classes of Services in use by Android. The first and what we'll be exploring is called a Local Service. Local Services exist as part of a specific application and are accessed and controlled only by that application. Remote Services are the other type. They can communicate with, be accessed, and be controlled by other applications. As mentioned, we'll be concentrating on using a Local Service to provide audio playback capabilities. Developing Remote Services is a very large topic and is unfortunately out of the scope of this book.
To demonstrate a Service, let's go through this very simple example.
First we'll need an activity with an interface that allows us to start and stop the Service.
package com.apress.proandroidmedia.ch06.simpleservice;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class SimpleServiceActivity extends Activity implements OnClickListener {
Our activity will have two Buttons—one for starting the Service and one for stopping it.
Button startServiceButton;
Button stopServiceButton;
In order to start or stop the Service, we use a standard intent.
Intent serviceIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Our activity implements OnClickListener
, so we set the OnClickListener
for each of the Buttons to be “this”.
startServiceButton = (Button) this.findViewById(R.id.StartServiceButton);
stopServiceButton = (Button) this.findViewById(R.id.StopServiceButton);
startServiceButton.setOnClickListener(this);
stopServiceButton.setOnClickListener(this);
When instantiating the intent that will be used to start and stop the Service, we pass in our activity as the Context followed by the Service's class.
serviceIntent = new Intent(this, SimpleServiceService.class);
}
public void onClick(View v) {
if (v == startServiceButton) {
When the startServiceButton
is clicked, we call the startService
method, passing in the intent just created to refer to our Service. The startService
method is a part of the Context class of which activity is a child.
startService(serviceIntent);
}
else if (v == stopServiceButton) {
When the stopServiceButton
is clicked, we call the stopService
method, passing in the same intent that refers to our Service. As with startService
, stopService
is part of the Context class.
stopService(serviceIntent);
}
}
}
Here is our main.xml
file that defines the layout for the foregoing activity. It contains the StartService and StopService Buttons as well as a TextView.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Simple Service"
/>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/StartServiceButton" android:text="Start Service"></Button>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Stop Service" android:id="@+id/StopServiceButton"></Button>
</LinearLayout>
Now we can move on to the code for the Service itself. In this example, we aren't accomplishing anything in the Service, just using Toast to tell us when the Service has started and stopped.
package com.apress.proandroidmedia.ch06.simpleservice;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
Services extend the android.app.Service
class. The Service
class is abstract, so in order to extend it, we have to at the very least implement the onBind
method. In this very simple example, we aren't going to be “binding” to the Service. Therefore we'll just return null
in our onBind
method.
public class SimpleServiceService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
The next three methods represent the Service's life cycle. The onCreate
method, as in the Activity, is called when the Service is instantiated. It will be the first method called.
@Override
public void onCreate() {
Log.v("SIMPLESERVICE","onCreate");
}
The onStartCommand
method will be called whenever startService
is called with an intent that matches this Service. Therefore it may be called more than once. The onStartCommand
returns an integer value that represents what the OS should do if it kills the Service. START_STICKY
, which we are using here, indicates that the Service will be restarted if killed.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v("SIMPLESERVICE","onStartCommand");
return START_STICKY;
}
ONSTARTCOMMAND VS. ONSTART
The onDestroy
method is called when the OS is destroying a Service. In this example, it is triggered when the stopService
method is called by our activity. This method should be used to do any cleanup that needs to be done when a Service is being shut down.
public void onDestroy() {
Log.v("SIMPLESERVICE","onDestroy");
}
}
Finally, in order to make this example work, we need to add an entry to our manifest file (AndroidManifest.xml
) that specifies our Service.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.apress.proandroidmedia.ch06.simpleservice"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".SimpleServiceActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".SimpleServiceService" />
</application>
<uses-sdk android:minSdkVersion="5" />
</manifest>
Of course, this example doesn't do anything other than output to the log indicating when the Service has been started and stopped. Let's move forward and have our Service actually do something.
Now that we have created an example Service, we can use it as a template to create an application to play audio files in the background. Here is a Service, and an activity to control the Service that does just that, allows us to play audio files in the background. It works in a similar manner to the custom audio player example from the last chapter, as it is using the same underlying MediaPlayer
class that Android makes available to us.
package com.apress.proandroidmedia.ch06.backgroundaudio;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.IBinder;
import android.util.Log;
The Service implements OnCompletionListener
so that it can be notified when the MediaPlayer
has finished playing an audio file.
public class BackgroundAudioService extends Service implements OnCompletionListener
{
We declare an object of type MediaPlayer
. This object will handle the playback of the audio as shown in the custom audio player example in the last chapter.
MediaPlayer mediaPlayer;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
Log.v("PLAYERSERVICE","onCreate");
In the onCreate
method, we instantiate the MediaPlayer
. We are passing it a specific reference to an audio file called goodmorningandroid.mp3
, which should be placed in the raw resources (res/raw
) directory of our project. If this directory doesn't exist, it should be created. Putting the audio file in that location allows us to refer to it by a constant in the generated R
class, R.raw.goodmorningandroid
. More detail about placing audio in the raw resources directory is available in Chapter 5 in the “Creating a Custom Audio-Playing Application” section.
mediaPlayer = MediaPlayer.create(this, R.raw.goodmorningandroid);
We also set our Service, this class, to be the OnCompletionListener
for the MediaPlayer
object.
mediaPlayer.setOnCompletionListener(this);
}
When the startService
command is issued on this Service, the onStartCommand
method will be triggered. In this method, we first check that the MediaPlayer
object isn't already playing, as this method may be called multiple times, and if it isn't, we start it. Since we are using the onStartCommand
method rather than the onStart
method, this example runs only in Android 2.0 and above.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v("PLAYERSERVICE","onStartCommand");
if (!mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
return START_STICKY;
}
When the Service is destroyed, the onDestroy
method is triggered. Since this doesn't guarantee that the MediaPlayer
will stop playing, we invoke its stop
method here if it is playing and also call its release
method to get rid of any memory usage and or resource locks.
public void onDestroy() {
if (mediaPlayer.isPlaying())
{
mediaPlayer.stop();
}
mediaPlayer.release();
Log.v("SIMPLESERVICE","onDestroy");
}
Because we are implementing OnCompletionListener
and the Service itself is set to be the MediaPlayer
's OnCompletionListener
, the following onCompletion
method is called when the MediaPlayer
has finished playing an audio file. Since this Service is only meant to play one song and that's it, we call stopSelf
, which is analogous to calling stopService
in our activity.
Here is the activity that corresponds to the foregoing Service. It has a very simple interface with two buttons to control the starting and stopping of the Service. In each case, it calls finish directly after to illustrate that the Service is not dependent on the activity and runs independently.
package com.apress.proandroidmedia.ch06.backgroundaudio;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class BackgroundAudioActivity extends Activity implements OnClickListener {
Button startPlaybackButton, stopPlaybackButton;
Intent playbackServiceIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
startPlaybackButton = (Button) this.findViewById(R.id.StartPlaybackButton);
stopPlaybackButton = (Button) this.findViewById(R.id.StopPlaybackButton);
startPlaybackButton.setOnClickListener(this);
stopPlaybackButton.setOnClickListener(this);
playbackServiceIntent = new Intent(this,BackgroundAudioService.class);
}
public void onClick(View v) {
if (v == startPlaybackButton) {
startService(playbackServiceIntent);
finish();
} else if (v == stopPlaybackButton) {
stopService(playbackServiceIntent);
finish();
}
}
}
Here is the main.xml
layout XML file in use by the foregoing activity.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Background Audio Player"
/>
<Button android:text="Start Playback" android:id="@+id/StartPlaybackButton"
android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
<Button android:text="Stop Playback" android:id="@+id/StopPlaybackButton"
android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
</LinearLayout>
Finally, here is the AndroidManifest.xml
for this project.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.apress.proandroidmedia.ch06.backgroundaudio"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".BackgroundAudioActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".BackgroundAudioService" />
</application>
<uses-sdk android:minSdkVersion="5" />
</manifest>
As shown, simply using the MediaPlayer
to start and stop audio playback within a Service is very straightforward. Let's now look at how we can take that a step further.
Unfortunately, when using a Service, issuing commands to the MediaPlayer from the user-facing activity becomes more complicated.
In order to allow the MediaPlayer
to be controlled, we need to bind the activity and Service together. Once we do that, since the activity and Service are running in the same process, we can call methods in the Service directly. If we were creating a remote Service, we would have to take further steps.
Let's add a Button labeled “Have Fun” to the foregoing activity. When this Button is pressed, we'll have the MediaPlayer
seek back a few seconds and continuing playing the audio file.
We'll start by adding the Button to the layout XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Background Audio Player"
/>
<Button android:text="Start Playback" android:id="@+id/StartPlaybackButton"
android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
<Button android:text="Stop Playback" android:id="@+id/StopPlaybackButton"
android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
<Button android:text="Have Fun" android:id="@+id/HaveFunButton"
android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
</LinearLayout>
Then in the activity, we'll get a reference to it and set its onClickListener
to be the activity itself, just like the existing Buttons.
package com.apress.proandroidmedia.ch06.backgroundaudiobind;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class BackgroundAudioActivity extends Activity implements OnClickListener {
Button startPlaybackButton, stopPlaybackButton;
Button haveFunButton;
Intent playbackServiceIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
startPlaybackButton = (Button) this.findViewById(R.id.StartPlaybackButton);
stopPlaybackButton = (Button) this.findViewById(R.id.StopPlaybackButton);
haveFunButton = (Button) this.findViewById(R.id.HaveFunButton);
startPlaybackButton.setOnClickListener(this);
stopPlaybackButton.setOnClickListener(this);
haveFunButton.setOnClickListener(this);
playbackServiceIntent = new Intent(this,BackgroundAudioService.class);
}
In order for us to have this Button interact with the MediaPlayer
that is running in the Service, we have to bind to the Service. The means to do this is with the bindService
method. This method takes in an intent; in fact, we can re-use the playbackServiceIntent
that we are using to start the Service, a ServiceConnection
object, and some flags for what to do when the Service isn't running.
In the following onClick
method in our activity, we bind to the Service right after we start it, when the startPlaybackButton
is pressed. We unbind from the Service when the stopPlaybackButton
is called.
public void onClick(View v) {
if (v == startPlaybackButton) {
startService(playbackServiceIntent);
bindService(playbackServiceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
} else if (v == stopPlaybackButton) {
unbindService(serviceConnection);
stopService(playbackServiceIntent);
}
You'll probably notice that we are using a new object that we haven't defined, serviceConnection
. This we'll take care of in a moment.
We also need to finish off the onClick
method. Since our new Button has its onClickListener
set to be the activity, we should handle that case as well and close out the onClick
method.
else if (v == haveFunButton) {
baService.haveFun();
}
}
In the new section, we are using another new object, baService
. baService
is an object that is of type BackgroundAudioService
. We'll declare it now and take care of creating when we create our ServiceConnection
object.
private BackgroundAudioService baService;
As mentioned, we are still missing the declaration and instantiation of an object called serviceConnection
. serviceConnection
will be an object of type ServiceConnection
, which is an Interface for monitoring the state of a bound Service.
Let's take care of creating our serviceConnection
now:
private ServiceConnection serviceConnection = new ServiceConnection() {
The onServiceConnected
method shown here will be called when a connection with the Service has been established through a bindService
command that names this object as the ServiceConnection
(such as we are doing in our bindService
call).
One thing that is passed into this method is an IBinder
object that is actually created and delivered from the Service itself. In our case, this IBinder
object will be of type BackgroundAudioServiceBinder
, which we'll create in our Service. It will have a method that returns our Service itself, called getService
. The object returned by this can be operated directly on, as we are doing when the haveFunButton
is clicked.
public void onServiceConnected(ComponentName className, IBinder baBinder) {
baService =
((BackgroundAudioService.BackgroundAudioServiceBinder)baBinder).getService();
}
We also need an onServiceDisconnected
method to handle cases when the Service goes away.
public void onServiceDisconnected(ComponentName className) {
baService = null;
}
};
}
.
Now we can turn our attention to what we need to change in the Service itself.
package com.apress.proandroidmedia.ch06.backgroundaudiobind;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class BackgroundAudioService extends Service implements OnCompletionListener
{
MediaPlayer mediaPlayer;
The first change that we'll need to make in our Service is to create an inner class that extends Binder
that can return our Service itself when asked.
public class BackgroundAudioServiceBinder extends Binder {
BackgroundAudioService getService() {
return BackgroundAudioService.this;
}
}
Following that, we'll instantiate that as an object called basBinder
.
private final IBinder basBinder = new BackgroundAudioServiceBinder();
And override the implementation of onBind
to return that.
@Override
public IBinder onBind(Intent intent) {
// Return the BackgroundAudioServiceBinder object
return basBinder;
}
That's it for the binding. Now we just need to deal with “Having Fun.”
As mentioned, when the haveFunButton
is clicked, we want the MediaPlayer
to seek back a few seconds. In this implementation, it will seek back 2,500 milliseconds or 2.5 seconds.
public void haveFun() {
if (mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() - 2500);
}
}
That's it for updates on the Service. Here is the rest of the code for good measure.
@Override
public void onCreate() {
Log.v("PLAYERSERVICE","onCreate");
mediaPlayer = MediaPlayer.create(this, R.raw.goodmorningandroid);
mediaPlayer.setOnCompletionListener(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v("PLAYERSERVICE","onStartCommand");
if (!mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
return START_STICKY;
}
public void onDestroy() {
if (mediaPlayer.isPlaying())
{
mediaPlayer.stop();
}
mediaPlayer.release();
Log.v("SIMPLESERVICE","onDestroy");
}
public void onCompletion(MediaPlayer _mediaPlayer) {
stopSelf();
}
}
Finally, here is the AndroidManifest.xml
file that is required by the foregoing example.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0" package="com.apress.proandroidmedia
.ch06.backgroundaudiobind">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".BackgroundAudioActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".BackgroundAudioService" />
</application>
<uses-sdk android:minSdkVersion="5" />
</manifest>
Now that the foundation is in place, we can add whatever functionality we like into the Service and call the various methods such as haveFun
directly from our activity by binding to it. Without binding to the Service, we would be unable to do anything more than start and stop the Service.
The foregoing examples should give a good starting point for building an application that plays audio files in the background, allowing users to continue doing other tasks while the audio continues playing. The second example can be extended to build a full-featured audio playback application.
18.227.26.217