第十二个项目——音乐播放器3
实验目的
了解Service的基本使用方法;
了解Notification发送通知的方法;
实验要求
使用Service实现音乐的后台播放;
使用Notification与Foreground Service实现音乐的后台播放;
实验内容
在第十一个Android项目的基础上,自定义名为MusicService的服务类,将MediaPlayer进行封装,在该服务类中实现当GGMusic应用被切换到后台运行或MainActivity活动返回后可继续进行音乐播放,项目最终运行效果如图1. 音乐播放器3运行效果所示。

步骤一,新建MusicService服务类
打开GGMusic项目,在Project窗口中右击app文件夹,选择【New】->【Service】->【Service】菜单,在弹出的New Android Component对话框中输入MusicService作为类名,将取消勾选exported复选框,选中enabled复选框,点击【Finish】完成MusicService服务类的创建工作,具体如图2. New Android Component新建MusicService所示。

通过New Android Component对话框新建MusicService后,Android Studio会在GGMusic的AndroidManifest.xml配置清单文件中注册MusicService组件,代码如下所示。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.glriverside.xgqin.ggmusic">
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
...>
<activity android:name=".MainActivity">
...
</activity>
<service android:name=".MusicService"
android:enabled="true"
android:exported="false">
</service>
</application>
</manifest>
步骤二,重载MusicService的onCreate()、onDestroy()方法
打开MusicService.java文件,默认会包含三个重载的方法:
onCreate(),服务被创建时执行的方法;
onDestroy(),服务结束时执行的方法;
onBind(),服务被绑定时执行的方法,在本项目中暂不对其进行讨论;
在本项目中,使用MusicService进行音乐播放,需要在MusicService服务中使用MediaPlayer对多媒体音乐文件进行操作。 对于MediaPlayer对象的管理,可在onCreate()、onDestroy()方法中实现其对象初始化及对象释放操作。 因此,在MusicService服务类中,首先定义名为mMediaPlayer的私有成员变量,在onCreate()方法中进行对象初始化操作,而在onDestroy()方法中则进行对象释放操作。
public class MusicService extends Service {
...
MediaPlayer mMediaPlayer;
public MusicService() {
}
@Override
public void onDestroy() {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
super.onDestroy();
}
@Override
public void onCreate() {
super.onCreate();
mMediaPlayer = new MediaPlayer();
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}
步骤三,重载MusicService的onStartCommand方法
服务可以在其他组件中进行调用,通常以startService()方法的形式进行实现。该方法接收一个Intent类型的参数,包含所需启动服务的信息及传递给服务的数据。而服务中的onStartCommand()方法则是用于响应startService()方法的回调方法,该方法包含的参数为:
Intent intent,由startService()方法传递过来的Intent对象,可以使用它获取其他组件传递过来的数据;
int flags,与特定服务请求相关的额外数据,其值是0或是STARTFLAG REDELIVERY或STAR_FLAG_RETRY的组合;
int startId,唯一的整型id,表示特定的一次startService()请求,可以在stopSelf(int)中使用,指定停止特定的一次服务;
在本项目中,通过修改ListView.OnItemClickListener接口中onItemClick()的回调方法,调用startService()启动MusicService活动,并传递所需播放的多媒体音乐文件路径,供MusicService类中的MediaPlayer对象播放。
1. 修改ListView.OnItemClickListener接口
在该接口的onItemClick()方法中,不再直接操作MainActivity活动中的MediaPlayer对象播放音乐,而是通过构造Intent对象,启动MusicService活动的方式支持后台播放音乐,代码如下所示。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
public static final String DATA_URI =
"com.glriverside.xgqin.ggmusic.DATA_URI";
...
private ListView.OnItemClickListener itemClickListener =
new ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int i, long l) {
Cursor cursor = mCursorAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(i)) {
...
Uri dataUri = Uri.parse(data);
Intent serviceIntent = new Intent(MainActivity.this,
MusicService.class);
serviceIntent.putExtra(MainActivity.DATA_URI, data);
startService(serviceIntent);
...
}
}
};
...
}
2. 重载MusicService服务类的onStartCommand方法
通过startService()方法中的Intent对象已经包含了需要播放的多媒体音乐文件的路径,因此可以在onStartCommand()方法中取出该值并构造对应的Uri对象,而播放音乐的代码则直接从ListView.OnItemClickListener的onItemClick()直接剪切过来,代码如下所示。
public class MusicService extends Service {
....
@Override
public int onStartCommand(Intent intent,
int flags, int startId) {
String data = intent.getStringExtra(
MainActivity.DATA_URI);
Uri dataUri = Uri.parse(data);
if (mMediaPlayer != null) {
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(
getApplicationContext(),
dataUri);
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return super.onStartCommand(intent, flags, startId);
}
}
步骤四,编译项目运行App
完成上述步骤后,可以编译GGMusic项目并运行App,点击某一首音乐并通过返回按钮退出GGMusic应用或者点击home按键退出到主屏幕,此时音乐应可在后台播放。
但在音乐未播放完时会出现中断播放的情况,这是由于MusicService服务虽然在后台运行,但仍然处在App的主线程中,而此时App处于后台运行,仍会被Android系统杀掉。解决此问题的办法有:
在MusicService中启动子线程,在子线程中实现音乐播放操作;
使用Foreground Service的形式运行MusicService,确保服务可长期运行;
步骤五,使用Foreground Service形式运行MusicService
Foreground Service也称为前台服务,与普通服务不同的是,前台服务对App使用者来说是可感知的。前台服务通常与Notification结合使用,当启动一个前台服务时,需要给用户发送一个Notification,用户通过该Notification可对服务进行控制。
对于调用前台服务的组件而言,只要使用startForegroundService()方法即可启动前台服务,但服务在响应该方法的onStartCommand()方法中需要自行创建一个Notification对象,并调用startForeground()方法显式的作为前台服务运行。
1. 修改ListView.OnItemClickListener接口的onItemClick()回调函数
将onItemClick()回调函数中的startService()方法,替换为startForegroundService()方法。由于MusicService要以前台服务形式运行,我们还需要通过Intent对象传递当前播放的歌曲名、歌手名等信息给MusicService,用于在Notification中显示当前播放的歌曲信息,具体代码如下所示。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
public static final String DATA_URI =
"com.glriverside.xgqin.ggmusic.DATA_URI";
public static final String TITLE =
"com.glriverside.xgqin.ggmusic.TITLE";
public static final String ARTIST =
"com.glriverside.xgqin.ggmusic.ARTIST";
...
private ListView.OnItemClickListener itemClickListener =
new ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int i, long l) {
Cursor cursor = mCursorAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(i)) {
...
Uri dataUri = Uri.parse(data);
Intent serviceIntent = new Intent(MainActivity.this,
MusicService.class);
serviceIntent.putExtra(MainActivity.DATA_URI, data);
serviceIntent.putExtra(MainActivity.TITLE, title);
serviceIntent.putExtra(MainActivity.ARTIST, artist);
startService(serviceIntent);
...
}
}
};
...
}
2. 修改MusicService的onStartCommand()方法
在onStartCommand()方法中,调用startForeground()方法将当前服务以前台服务形式运行,该方法需要两个参数:
int id,表示Notification的id,通常需要保存该id以便后续通过该id更新Notification;
Notification notification,Notification对象,表示通知实体;
因此在调用startForeground()方法前,需要构造Notification对象,可通过NotificationCompat.Builder静态类进行构造Notification对象。而在Android Oreo版本以上,创建Notification对象时需指定其所属的Notification Channel,因此还需要首先构造Notification Channel。
A 通过NotificationManager构造Notification Channel。首先通过Build.VERSION.SDK_INT可以判断当前系统的API版本,如果其大于Build.VERSION_CODES.O则表明需要构造Notification Channel。
B 通过NotificationCompat.Builder构造Notification对象。NotificationCompat.Builder提供了构造一个通知所需的所有方法,常用的方法包括:
setContentTitle(),设置Notification的标题,在此我们将当前播放的歌曲名作为标题;
setContentText(),设置Notification的正文文本,在此我们将当前播放的歌手名作为正文文本;
setSmallIcon(),设置Notification在系统状态栏上显示的图标;
setContentIntent(),设置用户点击通知时的Pending Intent(延迟意图);
其中setContentIntent()为通知设置延迟意图,当用户点击该通知时,通常希望能查看通知详情。在本项目中由于只有一个Activity,因此我们将该延迟意图设为打开MainActivity活动即可。延迟意图在Intent的基础上进行了封装,通过PendingIntent.geActivity()方法构造一个启动活动的延迟意图,具体代码如下所示。
C 通过startForeground()方法将当前的Service与Notification绑定,并以前台服务形式运行。
D 重新编译GGMusic项目并运行其App,点击ListView中某一首歌曲,你将会看到在系统状态栏中出现一个通知,该通知中显示了当前播放的音乐信息,点击该通知可以跳转至MainActivity活动页面。
public class MusicService extends Service {
private static final int ONGOING_NOTIFICATION_ID = 1001;
private static final String CHANNEL_ID = "Music channel";
NotificationManager mNotificationManager;
...
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mNotificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"Music Channel", NotificationManager.IMPORTANCE_HIGH);
if (mNotificationManager != null) {
mNotificationManager.createNotificationChannel(channel);
}
}
Intent notificationIntent =
new Intent(getApplicationContext(),
MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(
getApplicationContext(),
0, notificationIntent, 0);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(
getApplicationContext(),
CHANNEL_ID);
Notification notification = builder
.setContentTitle(title)
.setContentText(artist)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent).build();
startForeground(ONGOING_NOTIFICATION_ID, notification);
return super.onStartCommand(intent, flags, startId);
}
}
实验小结
通过本次实验,你应该掌握了如下知识内容:
掌握如何使用Service运行后台任务;
掌握如何在其他组件中启动Service;
掌握使用Foreground Service运行前台服务;
Last updated
Was this helpful?