> For the complete documentation index, see [llms.txt](https://xxgqin.gitbook.io/android/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xxgqin.gitbook.io/android/ch06/ch06-2.md).

# 第十二个项目——音乐播放器3

## 实验目的

* 了解**Service**的基本使用方法；
* 了解**Notification**发送通知的方法；

## 实验要求

* 使用**Service**实现音乐的后台播放；
* 使用**Notification**与**Foreground Service**实现音乐的后台播放；

## 实验内容

在第十一个**Android**项目的基础上，自定义名为**MusicService**的服务类，将**MediaPlayer**进行封装，在该服务类中实现当GGMusic应用被切换到后台运行或**MainActivity**活动返回后可继续进行音乐播放，项目最终运行效果如[图1. 音乐播放器3运行效果](/android/ch06/ch06-2.md#code12_screenshot)所示。

![图1. 音乐播放器3运行效果](http://www.funnycode.net/guet/img/ch06/code12_screenshot.png)

### 步骤一，新建MusicService服务类

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

![图2. New Android Component新建MusicService](http://www.funnycode.net/guet/img/ch06/code12_new_android_component.png)

通过**New Android Component**对话框新建**MusicService**后，**Android Studio**会在GGMusic的**AndroidManifest.xml**配置清单文件中注册**MusicService**组件，代码如下所示。

```markup
<?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()**&#x65B9;法中实现其对象初始化及对象释放操作。 因此，在**MusicService**服务类中，首先定义名为*mMediaPlayer*的私有成员变量，在**onCreate()**&#x65B9;法中进行对象初始化操作，而在**onDestroy()**&#x65B9;法中则进行对象释放操作。

```java
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()**&#x65B9;法的形式进行实现。该方法接收一个**Intent**类型的参数，包含所需启动服务的信息及传递给服务的数据。而服务中的**onStartCommand()**&#x65B9;法则是用于响应**startService()**&#x65B9;法的回调方法，该方法包含的参数为：

* *Intent intent*，由**startService()**&#x65B9;法传递过来的**Intent**对象，可以使用它获取其他组件传递过来的数据；
* *int flags*，与特定服务请求相关的额外数据，其值是*0*或是*STARTFLAG REDELIVERY*或*STAR\_FLAG\_RETRY*的组合；
* *int startId*，唯一的整型*id*，表示特定的一次**startService()**&#x8BF7;求，可以在**stopSelf(int)**&#x4E2D;使用，指定停止特定的一次服务；

在本项目中，通过修改**ListView\.OnItemClickListener**接口中**onItemClick()**&#x7684;回调方法，调用**startService()**&#x542F;动**MusicService**活动，并传递所需播放的多媒体音乐文件路径，供**MusicService**类中的**MediaPlayer**对象播放。

**1. 修改ListView\.OnItemClickListener接口**

在该接口的**onItemClick()**&#x65B9;法中，不再直接操作**MainActivity**活动中的**MediaPlayer**对象播放音乐，而是通过构造**Intent**对象，启动**MusicService**活动的方式支持后台播放音乐，代码如下所示。

```java
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()**&#x65B9;法中的**Intent**对象已经包含了需要播放的多媒体音乐文件的路径，因此可以在**onStartCommand()**&#x65B9;法中取出该值并构造对应的**Uri**对象，而播放音乐的代码则直接从**ListView\.OnItemClickListener**的**onItemClick()**&#x76F4;接剪切过来，代码如下所示。

```java
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()**&#x65B9;法即可启动前台服务，但服务在响应该方法的**onStartCommand()**&#x65B9;法中需要自行创建一个**Notification**对象，并调用**startForeground()**&#x65B9;法显式的作为前台服务运行。

**1. 修改ListView\.OnItemClickListener接口的onItemClick()回调函数**

将**onItemClick()**&#x56DE;调函数中的**startService()**&#x65B9;法，替换为**startForegroundService()**&#x65B9;法。由于**MusicService**要以前台服务形式运行，我们还需要通过**Intent**对象传递当前播放的歌曲名、歌手名等信息给**MusicService**，用于在**Notification**中显示当前播放的歌曲信息，具体代码如下所示。

```java
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()**&#x65B9;法中，调用**startForeground()**&#x65B9;法将当前服务以前台服务形式运行，该方法需要两个参数：

* *int id*，表示**Notification**的id，通常需要保存该id以便后续通过该id更新**Notification**；
* *Notification notification*，**Notification**对象，表示通知实体；

因此在调用**startForeground()**&#x65B9;法前，需要构造**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()**&#x4E3A;通知设置延迟意图，当用户点击该通知时，通常希望能查看通知详情。在本项目中由于只有一个**Activity**，因此我们将该延迟意图设为打开**MainActivity**活动即可。延迟意图在**Intent**的基础上进行了封装，通过**PendingIntent.geActivity()**&#x65B9;法构造一个启动活动的延迟意图，具体代码如下所示。

**C** 通过**startForeground()**&#x65B9;法将当前的**Service**与**Notification**绑定，并以前台服务形式运行。

**D** 重新编译GGMusic项目并运行其App，点击**ListView**中某一首歌曲，你将会看到在系统状态栏中出现一个通知，该通知中显示了当前播放的音乐信息，点击该通知可以跳转至**MainActivity**活动页面。

```java
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**运行前台服务；
