# 第十二个项目——音乐播放器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**运行前台服务；


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xxgqin.gitbook.io/android/ch06/ch06-2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
