实验目的
实验要求
使用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 组件,代码如下所示。
Copy <?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 文件,默认会包含三个重载的方法:
onBind() ,服务被绑定时执行的方法,在本项目中暂不对其进行讨论;
在本项目中,使用MusicService 进行音乐播放,需要在MusicService 服务中使用MediaPlayer 对多媒体音乐文件进行操作。 对于MediaPlayer 对象的管理,可在onCreate() 、onDestroy() 方法中实现其对象初始化及对象释放操作。 因此,在MusicService 服务类中,首先定义名为mMediaPlayer 的私有成员变量,在onCreate() 方法中进行对象初始化操作,而在onDestroy() 方法中则进行对象释放操作。
Copy 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 活动的方式支持后台播放音乐,代码如下所示。
Copy 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() 直接剪切过来,代码如下所示。
Copy 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 中显示当前播放的歌曲信息,具体代码如下所示。
Copy 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 活动页面。
Copy 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);
}
}
实验小结
通过本次实验,你应该掌握了如下知识内容:
掌握使用Foreground Service 运行前台服务;