2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > Android实现音乐播放器 Service后台播放Notification控制播放状态

Android实现音乐播放器 Service后台播放Notification控制播放状态

时间:2020-06-06 20:15:38

相关推荐

Android实现音乐播放器 Service后台播放Notification控制播放状态

本音乐播放器的功能:

1.实现访问手机本地的.mp3音乐文件;
2.在主页中将访问的数据显示出来(这里简单点用ListView,用RecycleView当然也可以);
3.在播放音乐页面实现上一首、下一首、播放/暂停、进度条随音乐播放滑动、动态显示播放时间,拖动进度条,当前播放时间动态改变并且播放进度也会改变、自动切换下一首歌;
4.从在Activity中播放音乐过渡到在Service中播放音乐;
5.改进该项目,使用广播的方式播放音乐并能够在Notification中控制音乐的播放状态。

由于我将全部代码都贴在了本博客,所以文章篇幅较长。

我的最终效果图:

本项目效果图:

----------------------------------------以下就开始分别实现----------------------------------------
第一步:实现访问手机本地的.mp3音乐文件
编写一个音乐类Music(以周杰伦 - 最长的电影.mp3为例):编写四个成员变量,并在其中提供对应的set()和get()方法

//音乐的名字(周杰伦 - 最长的电影.mp3),截取后缀获得.mp3的文件,subString("-")截取歌曲的名字

private String name;

//音乐文件的作者

private String artist;

//音乐文件的路径

private String url;

//音乐播放的时间

private int time;

编写一个专门访问本地音乐文件的类MusicList,这里用到读取内存的权限,因此别忘了要在AndroidManifest.xml中添加权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

实现步骤:

①编写静态方法getMusicData(),返回值就是查询到的音乐数据集合;

②在该方法中创建ContentRecolver实例,通过上下文的getContentResolver()方法;

③判断获取到的ContentResolver是否为空,如果不为空,调用contentResolver.query (Uri uri, String[] projection,String selection,String[] selectionArgs, String sortOrder)方法查询本地的音乐文件,返回一个Cursor对象。如果Cursor为空,说明没有数据,直接返回null;

④如果有数据,利用Cursor对象的moveToFirst()查询第一条数据,如果不为空,就循环调用moveToNext()方法查询下一条数据直到没有数据为止;

⑤在查询中,通过cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.××))获取到相应的数据,该数据就对应等于Music中的成员变量;’

⑥将查询到的数据赋值到Music对象,完成查询数据的存储,并把该Music对象添加到Music集合,这样就获取到了本地音乐数据集合。

对上面的补充说明(对ContentResolver和ContentProvider了解的可直接跳过)

ContentProvider在android中的作用是对外共享数据,也就是说你可以通过 ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对你应用中的数据进行添删改查。

ContentResolver:提供访问ContentProvider的能力,可以使用ContentResolver读取和操作其他应用程序已经通过ContentProvider暴露出来的数据。

/*** 从内部存储中读取下载好的后缀名为.mp3的音乐文件,返回值为Music集合*/public class MusicList {public static ArrayList<Music> getMusicData(Context context){ArrayList<Music> musicList = new ArrayList<Music>();ContentResolver contentResolver = context.getContentResolver();if(contentResolver != null){Cursor cursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null,MediaStore.Audio.Media.DEFAULT_SORT_ORDER);if(cursor == null){return null;}if(cursor.moveToFirst()) {do {Music music = new Music();String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));String name = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME));if ("<unknown>".equals(artist)) {String[] split = name.split("-");artist = split[0];}int time = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));String url = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));String isMp3 = name.substring(name.length() - 3, name.length());if (isMp3.equals("mp3")) {music.setArtist(artist);music.setTime(time);music.setUrl(url);music.setName(name.substring(0,name.length()-4));musicList.add(music);}} while (cursor.moveToNext());}}return musicList;}}

第二步:在主页中将访问的数据显示出来(这里简单点用ListView,用RecycleView当然也可以)

实现步骤:

1.请求运行时权限(Android 6.0及以上系统在使用危险权限时都必须进行运行时权限的处理,这里如果不进行处理,会出现闪退的现象)

//自定义方法requestPermission()private void requestPermission() {//创建允许权限的集合List<String> permissionList = new ArrayList<String>();//如果该权限没同意,就加入到允许权限的集合if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);}if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);}//如果允许权限不为空,说明有权限还没申请,就一起申请if (!permissionList.isEmpty()) {ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), 1);}//权限申请完成之后,才去初始化视图else {//适配器的适配工作initView();}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {switch (requestCode) {case 1:if (grantResults.length > 0) {for (int i = 0; i < grantResults.length; i++) {int grantResult = grantResults[i];if (grantResult == PackageManager.PERMISSION_DENIED) {String s = permissions[i];Toast.makeText(this, s + "权限被拒绝了", Toast.LENGTH_SHORT).show();} else {initView();}}}}}

2.设置适配器显示数据

①编写主页的xml(activity_main.xml)

<LinearLayout xmlns:android="/apk/res/android"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity" ><ListViewandroid:id="@+id/listView_main"android:layout_width="match_parent"android:layout_height="wrap_content" /></LinearLayout>

②在主页中获取到该控件,并给该控件设置适配器进行显示

MusicAdapter

public class MusicAdapter extends BaseAdapter {private Context mContext;private List<Music> mMusicList;public MusicAdapter(Context context,List<Music> musicList){mContext = context;mMusicList = musicList;}@Overridepublic int getCount() {return mMusicList.size();}@Overridepublic Object getItem(int position) {return mMusicList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder viewHolder;Music music = mMusicList.get(position);if (convertView == null) {convertView = LayoutInflater.from(mContext).inflate(R.layout.music_item, parent,false);viewHolder = new ViewHolder();viewHolder.music_item_name = convertView.findViewById(R.id.music_item_name);viewHolder.music_item_artist = convertView.findViewById(R.id.music_item_artist);viewHolder.music_item_time = convertView.findViewById(R.id.music_item_time);convertView.setTag(viewHolder);} else {viewHolder = (ViewHolder) convertView.getTag();}viewHolder.music_item_name.setText(music.getName());viewHolder.music_item_artist.setText(music.getArtist());viewHolder.music_item_time.setText(formatTime(music.getTime()));return convertView;}class ViewHolder {TextView music_item_name;TextView music_item_artist;TextView music_item_time;}//将时间转换为××:××格式private String formatTime(int time) {int ms2s = (time / 1000);int minute = ms2s / 60;int second = ms2s % 60;return String.format("%02d:%02d", minute, second);}}

music.item.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="16dp"><TextViewandroid:id="@+id/music_item_name"android:layout_width="match_parent"android:layout_height="wrap_content" /><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="5dp"><TextViewandroid:id="@+id/music_item_artist"android:layout_width="wrap_content"android:layout_height="wrap_content" /><TextViewandroid:id="@+id/music_item_time"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="10dp"android:layout_toRightOf="@id/music_item_artist" /></RelativeLayout></LinearLayout>

MainActivity

public class MainActivity extends AppCompatActivity {private ArrayList<Music> arrayList;private MusicAdapter mMusicAdapter;private ListView mListView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);requestPermission();}private void initView() {arrayList = MusicList.getMusicData(this);mListView = findViewById(R.id.listView_main);mMusicAdapter = new MusicAdapter(this, arrayList);mListView.setAdapter(mMusicAdapter);//实现页面跳转,将当前点击的音乐条目传过去给音乐播放页面,这样它才知道播放哪条音乐mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {//Intent intent = new Intent(MainActivity.this,DetailActivity.class);//Bundle bundle = new Bundle();//bundle.putInt("position",position);//intent.putExtras(bundle);//startActivity(intent);}});}private void requestPermission() {...}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {...}}

到此为止,就能实现将本地音乐显示到主页的效果了。接着,开始实现音乐播放页面(DetailActivity)

第三步:在播放音乐页面实现上一首、下一首、播放/暂停、进度条随音乐播放滑动、用手拖动进度条,当前播放时间动态改变并且播放进度也会改变、自动切换下一首歌
①编写播放页面的布局(activity_detail.xml)

<LinearLayout xmlns:android="/apk/res/android"android:id="@+id/LinearLayout1"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffb6c1"android:orientation="vertical"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="3"><ImageViewandroid:id="@+id/btn_return"android:layout_width="50dp"android:layout_height="50dp"android:layout_gravity="bottom"android:src="@drawable/return_img" /></RelativeLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="2"android:orientation="vertical"><SeekBarandroid:id="@+id/seekBar"android:layout_width="match_parent"android:layout_height="wrap_content" /><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="10dp"><TextViewandroid:id="@+id/tv_cur_time"android:layout_width="wrap_content"android:layout_height="wrap_content" /><TextViewandroid:id="@+id/tv_total_time"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true" /></RelativeLayout><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:orientation="horizontal"><Buttonandroid:id="@+id/btn_pre"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="上一首" /><Buttonandroid:id="@+id/btn_play"android:layout_width="match_parent"android:layout_height="wrap_content"android:onClick="play"android:text="暂停" /><Buttonandroid:id="@+id/btn_next"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="下一首" /></LinearLayout></LinearLayout></LinearLayout>

②初始化各个控件

public class DetailActivity extends AppCompatActivity implements View.OnClickListener {private Button btn_pre, btn_play, btn_next;private TextView tv_cur_time, tv_total_time;private ImageView btn_return;private SeekBar seekBar;private ArrayList<Music> mMusicList;private int mPosition;private MediaPlayer mMediaPlayer;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_detail);initView();initData();}private void initData() {mMediaPlayer = new MediaPlayer();//拿到播放音乐的位置mPosition = getIntent().getIntExtra("position", -1);//拿到音乐数据集合mMusicList = MusicList.getMusicData(this);}private void initView() {btn_pre = findViewById(R.id.btn_pre);btn_play = findViewById(R.id.btn_play);btn_next = findViewById(R.id.btn_next);btn_return = findViewById(R.id.btn_return);tv_cur_time = findViewById(R.id.tv_cur_time);tv_total_time = findViewById(R.id.tv_total_time);seekBar = findViewById(R.id.seekBar);btn_pre.setOnClickListener(this);btn_play.setOnClickListener(this);btn_next.setOnClickListener(this);btn_return.setOnClickListener(this);}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btn_pre:break;case R.id.btn_play:break;case R.id.btn_next:break;case R.id.btn_return:onBackPressed();break;}}}

③开始编写方法
1.播放音乐的方法

public void playMusic(int position) {try {mMediaPlayer.reset();mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());mMediaPlayer.prepare();mMediaPlayer.start();} catch (IOException e) {e.printStackTrace();}}

2.播放/暂停的方法

public void play() {if (mMediaPlayer.isPlaying()) {mMediaPlayer.pause();btn_play.setText("播放");} else {mMediaPlayer.start();btn_play.setText("暂停");}}

3.下一曲/上一曲的方法

public void next(int offset) {mPosition += offset;mPosition = (mMusicList.size() + mPosition) % mMusicList.size();playMusic(mPosition);}

在按钮点击事件中调用,就可以播放,暂停,下一曲,上一曲。为了让界面一进入就能够播放音乐,可以在onCreate()方法中调用playMusic(mPosition),不过记得要在initData()之后,也就是获取到mPosition之后,mPosition就是在MainActivity中通过ListView的ItemClickListener将位置传过来。

别忘了DetailActivity要在AndroidManifest中注册!!

④进度条随音乐播放滑动、动态显示播放时间

更新操作需要单独开一个线程执行,将工作线程需操作UI的消息传递到主线程,使得主线程可根据工作线程的需求更新UI。这是由于在Android开发中,为了UI操作是线程安全的,规定了只允许在主线程中更新UI。因此,在主线程中编写Handler。

实现步骤较简单,直接看代码。

消息应该在什么时候发送呢?

应该在播放音乐playMusic()中调用handler.sendEmptyMessage(0x01),也就是一播放音乐就开启调用。

private Handler handler = new Handler(){@Overridepublic void handleMessage(Message msg) {if(msg.what == 0x01){tv_cur_time.setText("00:00");//这里的格式化时间在前面已有编写tv_cur_time.setText(formatTime(mMediaPlayer.getCurrentPosition()));tv_total_time.setText(formatTime(mMusicList.get(mPosition).getTime()));seekBar.setProgress(mMediaPlayer.getCurrentPosition());seekBar.setMax(mMusicList.get(mPosition).getTime());handler.sendEmptyMessage(0x01);}}};

⑤拖动进度条,当前播放时间动态改变并且播放进度也会改变

需要用到seekbar的setOnSeekBarChangeListener()方法

当调用其中的onStartTrackingTouch()方法时,表示seekbar开始滑动了,将自定义的变量isSeekBarChanging设为true;当调用其中的onStopTrackingTouch()方法时,表示seekbar结束滑动了,将自定义的变量 isSeekBarChanging设为false。既然结束滑动了,那就要开始更改当前播放进度,通过调用mMediaPlayer.seekTo(seekBar.getProgress())方法完成;

seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {isSeekBarChanging = true;}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {isSeekBarChanging = false;mMediaPlayer.seekTo(seekBar.getProgress());}});

添加到handleMessage()方法中即可。

⑦自动切换下一首歌

mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {next(1);Toast.makeText(DetailActivity.this, "自动为您切换下一首:"+mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();}});

同样,将其添加到handleMessage()方法中。到此,播放功能已全部实现。

==不要着急,我们试着退出播放音乐的页面,再重新选择歌曲播放,会发现可以同时播放多条音乐,且他们之间互不影响,为什么呢?

这是因为我们的MediaPlayer每次使用完后都没有释放资源,每点击一个音乐条目进来就重新创建了一个MediaPlayer。那么如何做呢?

思路:根据两次传进来的position的异同,决定是否释放上次的MediaPlayer资源

创建一个静态的MediaPlayer,保存上次的MediaPlayer

创建一个静态的position,保存上次的position

每次播放音乐的时候,如果第一次传进来的position和第二次传进来的position不一样,说明不是同一个音乐文件,那么MediaPlayer就要进行一系列操作。首先创建一个新的MediaPlayer,把上一次的MediaPlayer资源释放掉,将本次的MediaPlayer赋值为上一次的MediaPlayer,将本次的position赋值为上次的position。

每次播放音乐的时候,如果第一次传进来的position和第二次传进来的position一样,说明是同一个音乐文件,那么MediaPlayer就不需要做任何操作,直接将上次的MediaPlayer赋值给本次的MediaPlayer,上次的position赋值给本次的position,这样就不用去创建新的MediaPlayer,直接拿到上次的position。

这里有一点需要注意,现在MediaPlayer的创建时放在了playMusic()方法中,因为每次播放音乐都要判断是否需要创建新的MediaPlayer,而且用handler处理message也加了个判断,具体更改见代码:

public class DetailActivity extends AppCompatActivity implements View.OnClickListener {private Button btn_pre, btn_play, btn_next;private TextView tv_cur_time, tv_total_time;private ImageView btn_return;private SeekBar seekBar;private ArrayList<Music> mMusicList;private int mPosition;static int savePosition;private MediaPlayer mMediaPlayer;static MediaPlayer mPreMediaPlayer;private boolean isSeekBarChanging;private Handler handler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 0x01 && mPosition == savePosition) {tv_cur_time.setText("00:00");tv_cur_time.setText(formatTime(mMediaPlayer.getCurrentPosition()));tv_total_time.setText(formatTime(mMusicList.get(mPosition).getTime()));seekBar.setProgress(mMediaPlayer.getCurrentPosition());seekBar.setMax(mMusicList.get(mPosition).getTime()); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {isSeekBarChanging = true;}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {isSeekBarChanging = false;mMediaPlayer.seekTo(seekBar.getProgress());}});mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {next(1);Toast.makeText(DetailActivity.this, "自动为您切换下一首:" + mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();}});handler.sendEmptyMessage(0x01);}}};@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_detail);initView();initData();if (mPreMediaPlayer == null || mPosition != savePosition) {playMusic(mPosition);} else {mMediaPlayer = mPreMediaPlayer;mPosition = savePosition;handler.sendEmptyMessage(0x01);}// playMusic(mPosition);}private void initData() {//拿到播放音乐的位置mPosition = getIntent().getIntExtra("position", -1);//拿到音乐数据集合mMusicList = MusicList.getMusicData(this);}private void initView() {btn_pre = findViewById(R.id.btn_pre);btn_play = findViewById(R.id.btn_play);btn_next = findViewById(R.id.btn_next);btn_return = findViewById(R.id.btn_return);tv_cur_time = findViewById(R.id.tv_cur_time);tv_total_time = findViewById(R.id.tv_total_time);seekBar = findViewById(R.id.seekBar);btn_pre.setOnClickListener(this);btn_play.setOnClickListener(this);btn_next.setOnClickListener(this);btn_return.setOnClickListener(this);}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btn_pre:next(-1);break;case R.id.btn_play:play();break;case R.id.btn_next:next(1);break;case R.id.btn_return:onBackPressed();break;}}public void playMusic(int position) {mMediaPlayer = new MediaPlayer();if (mPreMediaPlayer != null) {mPreMediaPlayer.stop();mPreMediaPlayer.release();}mPreMediaPlayer = mMediaPlayer;savePosition = mPosition;try {mMediaPlayer.reset();mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());mMediaPlayer.prepare();mMediaPlayer.start();} catch (IOException e) {e.printStackTrace();}handler.sendEmptyMessage(0x01);}public void play() {if (mMediaPlayer.isPlaying()) {mMediaPlayer.pause();btn_play.setText("播放");} else {mMediaPlayer.start();btn_play.setText("暂停");}}public void next(int offset) {mPosition += offset;mPosition = (mMusicList.size() + mPosition) % mMusicList.size();playMusic(mPosition);}private String formatTime(int time) {int ms2s = (time / 1000);int minute = ms2s / 60;int second = ms2s % 60;return String.format("%02d:%02d", minute, second);}}

到此为止,前三个功能已经全部实现,下面开始实现在Service中播放本地音乐

大体思路:

①创建自定义Service类继承自Service,因为需要用到绑定服务,所以要自定义MusicBinder类继承自Binder,然后在onBind()方法中返回我们自定义的MusicBinder类的实例,该实例定义了Activity可以与Service交互的程序接口

②在MusicBinder类中开始编写一系列操作音乐播放的代码,也就是把原来Activity中与MediaPlayer播放音乐有关的方法和与MediaPlayer操作有关的变量全部移植过来;

③在Activity中开启服务并绑定服务,这样就能访问到Service中程序接口定义的方法;

④如果Activity中需要用到MediaPlayer的方法接口中却没有,那么就在接口中添加自定义方法然后通过调用该接口即可。

⑤记得要在不使用Service的地方解绑该服务。

具体细节看更改后的代码:

MusicService

public class MusicService extends Service {private int mPosition;//静态存储上次音乐条目的位置static int savePosition;private MediaPlayer mMediaPlayer;//静态存储上次音乐条目的MediaPlayerstatic MediaPlayer mPreMediaPlayer;private ArrayList<Music> mMusicList;//静态存储当前音乐是否在播放static boolean isPlaying;public MusicService() {}//onCreate()只被执行一次,因此用来做初始化@Overridepublic void onCreate() {super.onCreate();mMusicList = MusicList.getMusicData(this);}class MusicBinder extends Binder {//创建一个绑定服务时,必须提供一个客户端与Service交互的IBinder,有三种方法//我使用的方法:返回当前Service实例,它具有一些客户端可以公开调用的公开方法public MusicService getService() {return MusicService.this;}}@Overridepublic IBinder onBind(Intent intent) {return new MusicBinder();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {mPosition = intent.getExtras().getInt("position", -1);if (mPreMediaPlayer == null || mPosition != savePosition) {playMusic(mPosition);} else {mMediaPlayer = mPreMediaPlayer;mPosition = savePosition;}return super.onStartCommand(intent, flags, startId);}/*** 播放音乐*/public void playMusic(int position) {mMediaPlayer = new MediaPlayer();if (mPreMediaPlayer != null) {mPreMediaPlayer.stop();mPreMediaPlayer.release();}mPreMediaPlayer = mMediaPlayer;savePosition = position;try {mMediaPlayer.reset();mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());mMediaPlayer.prepare();mMediaPlayer.start();} catch (IOException e) {e.printStackTrace();}mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {mPosition += 1;mPosition = (mMusicList.size() + mPosition) % mMusicList.size();playMusic(mPosition);Toast.makeText(getApplicationContext(), "自动为您切换下一首:" + mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();}});}/*** 按钮点击:播放音乐*/public void play() {if (mMediaPlayer.isPlaying()) {mMediaPlayer.pause();isPlaying = false;} else {mMediaPlayer.start();isPlaying = true;}}/*** 按钮点击:下一首*/public void next(int offset) {mPosition += offset;mPosition = (mMusicList.size() + mPosition) % mMusicList.size();playMusic(mPosition);}/*** 获取当前音乐的名字*/public String getName() {return mMusicList.get(mPosition).getName();}/*** 获取当前音乐的播放时间*/public int getTime() {return mMusicList.get(mPosition).getTime();}/*** 获取当前播放位置*/public int getCurrent() {return mMediaPlayer.getCurrentPosition();}/*** 设置音乐播放的进度*/public void seekTo(int progress) {mMediaPlayer.seekTo(progress);}}

DetailActivity

public class DetailActivity extends AppCompatActivity implements View.OnClickListener {private Button btn_pre, btn_play, btn_next;private TextView tv_cur_time, tv_total_time;private SeekBar seekBar;private ImageView btn_return;//seekBar是否被拖动private boolean isSeekBarChanging;private MusicService musicService;private Handler handler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 0x01) {tv_cur_time.setText("00:00");tv_cur_time.setText(formatTime(musicService.getCurrent()));tv_total_time.setText(formatTime(musicService.getTime()));seekBar.setProgress(musicService.getCurrent());seekBar.setMax(musicService.getTime());seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {isSeekBarChanging = true;}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {isSeekBarChanging = false;musicService.seekTo(seekBar.getProgress());}});handler.sendEmptyMessage(0x01);}}};@Overrideprotected void onDestroy() {super.onDestroy();unbindService(conn);handler.removeCallbacksAndMessages(null);}@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_detail);initView();Intent intent = new Intent(this, MusicService.class);//获取MainActivity传过来的数据Bundle bundle = getIntent().getExtras();//再把这些包装好的数据重新装入Intent,发送给Serviceintent.putExtras(bundle);startService(intent);bindService(intent, conn, BIND_AUTO_CREATE);}ServiceConnection conn = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {musicService = ((MusicService.MusicBinder) service).getService();handler.sendEmptyMessage(0x01);}@Overridepublic void onServiceDisconnected(ComponentName name) {}};/*** 按钮点击事件*/@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btn_pre:musicService.next(-1);break;case R.id.btn_play:musicService.play();break;case R.id.btn_next:musicService.next(1);break;case R.id.btn_return:onBackPressed();break;}updateBtnPlayOrPause();}private void updateBtnPlayOrPause() {if (MusicService.isPlaying) {btn_play.setText("暂停");} else {btn_play.setText("播放");}}private void initView() {...}private String formatTime(int time) {...}}

这里别忘了如果service不是通过Android Studio自动生成的,记得在AndroidManifest中声明。

第五步:使用广播的方式播放音乐并能够在Notification中控制音乐的播放状态

实现步骤:

①创建播放/暂停、上一曲、下一曲的动态广播;

②在按钮点击事件中发送对应的广播;

③编写广播接收器,对广播进行过滤。拿到想要的广播后,根据广播的类型进行操作;

④在服务开启的时候就开启广播接收器。

创建播放/暂停、上一曲、下一曲的动态广播

在DetailActivity中创建动态广播

/*** 按钮:播放音乐的广播*/public void playMusic(){Intent intent = new Intent();intent.setAction(MusicService.ACTION);Bundle bundle = new Bundle();bundle.putInt(MusicService.BTN_STATE,MusicService.PLAY_STATE);intent.putExtras(bundle);sendBroadcast(intent);}/*** 按钮:下一首音乐的广播*/public void nextMusic(){Intent intent = new Intent();intent.setAction(MusicService.ACTION);Bundle bundle = new Bundle();bundle.putInt(MusicService.BTN_STATE,MusicService.NEXT_MUSIC_STATE);intent.putExtras(bundle);sendBroadcast(intent);}/*** 按钮:上一首音乐的广播*/public void preMusic(){Intent intent = new Intent();intent.setAction(MusicService.ACTION);Bundle bundle = new Bundle();bundle.putInt(MusicService.BTN_STATE,MusicService.PRE_MUSIC_STATE);intent.putExtras(bundle);sendBroadcast(intent);}

编写广播接收器,过滤广播,拿到想要的广播并根据传过来的值进行相应的操作

在MusicService中编写广播接收器

private BroadcastReceiver receiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (intent.getAction().equals(ACTION)) {switch (intent.getIntExtra(BTN_STATE, -1)) {case PLAY_STATE:play();break;case PRE_MUSIC_STATE:next(-1);break;case NEXT_MUSIC_STATE:next(1);break;default:Toast.makeText(context, "系统出错,请稍后重试!", Toast.LENGTH_SHORT).show();break;}}}};

在服务开启的时候就开启广播接收器

在MusicService刚刚启动的时候(onCreate())就注册了一个广播,这样他就能够接收其他页面点击了上一曲、下一曲、暂停/播放按钮发出的广播。因此,其他页面中点击上一曲、下一曲、暂停、播放按钮时都需要向MusicService发送广播通知,让它来更新音乐。

IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(ACTION);registerReceiver(receiver,intentFilter);

同时我们还要更改一下之前的代码,在按钮点击事件中,之前我们是通过服务的方法去响应事件,我们都将其改为用广播的方式。也就是把musicService.next(-1)改为nextMusic(),musicService.play()改为playMusic()、musicService.next(1)改为nextMusic()。

这里用到的常量我也贴出代码来:

//动态广播的Actionpublic static String ACTION = "action";//按钮点击状态标识符public static String BTN_STATE = "btn_state";public static final int PLAY_STATE = 0, NEXT_MUSIC_STATE = 1, PRE_MUSIC_STATE = 2;

这样就可以实现以广播的方式播放音乐
紧接着实现通过Notification控制音乐的播放状态这一功能。

功能剖析:

首先需要先有Notification,所以在开启服务之后也就是onStartCommand()方法中,就先创建出Notification并进行一系列的初始化操作。

播放页面中按钮一点击就发送广播,广播接收器接收到广播,对音乐状态进行控制。并且播放页面中按钮的点击能够更改Notification中播放/暂停按钮显示的图片和当前播放的状态。

Notification中的按钮一点击就发送广播,广播接收器接收到广播,对音乐状态进行控制,并且Notification中按钮的点击能够更改播放页面中播放/暂停按钮显示的文字和当前播放的状态。

实现步骤:

①由于Notification布局本项目采用自定义,因此需要编写自定义的Notification布局RemoteViews;

②编写Notification初始化方法,这里需要在获取到position之后调用,因为RemoteViews需要根据position显示歌曲名字(这里我把Notification和Button的广播做成两个,方便大家逐块进行理解,所以代码比较累赘,可以考虑封装合并,读者有条件自行完成;)

③编写Notification的更新的方法并在每一次接收到按钮发出的广播的时候对Notification进行更新,这样按钮发出广播既实现了对音乐状态的控制,又实现了对Notification的更新;

④编写按钮的更新方法并在每一次接收到Notification发出的广播的时候对按钮进行更新,这样Notification发出的广播既实现了对音乐状态的控制,又实现了对按钮的更新。

以下是最终代码:

RemoteVeiws布局

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@drawable/background"android:orientation="horizontal"><ImageViewandroid:layout_marginLeft="8dp"android:layout_marginTop="8dp"android:layout_width="76dp"android:layout_height="76dp"android:src="@drawable/music" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="8dp"android:layout_marginRight="8dp"android:orientation="vertical"><TextViewandroid:id="@+id/tv_music_name"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="8dp"android:textSize="16sp"android:textColor="@android:color/white"android:textStyle="italic|bold"android:text="歌曲名" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/iv_pre_music"android:layout_marginRight="16dp"android:layout_width="48dp"android:layout_height="48dp"android:src="@drawable/pre" /><ImageViewandroid:id="@+id/iv_play_pause"android:layout_width="48dp"android:layout_height="48dp"android:src="@drawable/pause" /><ImageViewandroid:id="@+id/iv_next_music"android:layout_marginLeft="16dp"android:layout_width="48dp"android:layout_height="48dp"android:src="@drawable/next" /></LinearLayout></LinearLayout></LinearLayout>

public class DetailActivity extends AppCompatActivity implements View.OnClickListener {private ImageView iv_pre, iv_next, iv_play, btn_return;private TextView tv_cur_time, tv_total_time;private SeekBar seekbar;//seekBar是否被拖动private boolean isSeekBarChanging;private MusicService musicService;//按钮点击状态标识符public static String NOTIFICATION_ACTION = "notification_action";public static String NOTIFICATION_BTN_STATE = "notification_btn_state";public static final int NOTIFICATION_PLAY = 0, NOTIFICATION_NEXT_MUSIC = 1, NOTIFICATION_PRE_MUSIC = 2;private Handler handler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 0x01) {tv_cur_time.setText("00:00");tv_cur_time.setText(formatTime(musicService.getCurrent()));tv_total_time.setText(formatTime(musicService.getTime()));seekbar.setMax(musicService.getTime());seekbar.setProgress(musicService.getCurrent());seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {isSeekBarChanging = true;}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {isSeekBarChanging = false;musicService.seekTo(seekBar.getProgress());}});handler.sendEmptyMessage(0x01);}}};@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_detail);initView();initService();initBroadcastReceiver();}private void initBroadcastReceiver() {IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(NOTIFICATION_ACTION);registerReceiver(receiver,intentFilter);}private void initService() {Intent intent = new Intent(this, MusicService.class);//获取MainActivity传过来的数据Bundle bundle = getIntent().getExtras();//再把这些包装好的数据重新装入Intent,发送给Serviceintent.putExtras(bundle);startService(intent);bindService(intent, conn, BIND_AUTO_CREATE);}@Overrideprotected void onDestroy() {super.onDestroy();unbindService(conn);handler.removeCallbacksAndMessages(null);}/*** 广播接收器,接收来自Notification的广播*/private BroadcastReceiver receiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (intent.getAction().equals(NOTIFICATION_ACTION)) {switch (intent.getIntExtra(NOTIFICATION_BTN_STATE, -1)) {case NOTIFICATION_PRE_MUSIC:musicService.next(-1);break;case NOTIFICATION_PLAY:musicService.play();break;case NOTIFICATION_NEXT_MUSIC:musicService.next(1);break;default:Toast.makeText(context, "系统出错,请稍后重试!", Toast.LENGTH_SHORT).show();break;}updateNotificationBtnPlayOrPause();musicService.updateNotification();}}};private void updateNotificationBtnPlayOrPause() {if (MusicService.isPlaying) {iv_play.setImageResource(R.drawable.pause);} else {iv_play.setImageResource(R.drawable.play);}}ServiceConnection conn = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {musicService = ((MusicService.MusicBinder) service).getService();handler.sendEmptyMessage(0x01);}@Overridepublic void onServiceDisconnected(ComponentName name) {}};/*** 按钮点击事件*/@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.iv_pre:preMusic();MusicService.isPlaying = false;break;case R.id.iv_play:playMusic();break;case R.id.iv_next:nextMusic();MusicService.isPlaying = false;break;case R.id.btn_return:onBackPressed();break;}updateBtnPlayOrPause();}private void updateBtnPlayOrPause() {if (MusicService.isPlaying) {iv_play.setImageResource(R.drawable.play);} else {iv_play.setImageResource(R.drawable.pause);}}/*** 初始化视图*/private void initView() {iv_pre = findViewById(R.id.iv_pre);iv_play = findViewById(R.id.iv_play);iv_next = findViewById(R.id.iv_next);btn_return = findViewById(R.id.btn_return);tv_cur_time = findViewById(R.id.tv_cur_time);tv_total_time = findViewById(R.id.tv_total_time);seekbar = findViewById(R.id.seekbar);iv_pre.setOnClickListener(this);iv_play.setOnClickListener(this);iv_next.setOnClickListener(this);btn_return.setOnClickListener(this);}/*** 格式播放时间*/private String formatTime(int time) {int miao = (time /= 1000);int minute = miao / 60;int second = miao % 60;return String.format("%02d:%02d", minute, second);}/*** 按钮:播放音乐的广播*/public void playMusic() {Intent intent = new Intent();intent.setAction(MusicService.ACTION);Bundle bundle = new Bundle();bundle.putInt(MusicService.BTN_STATE, MusicService.PLAY_STATE);intent.putExtras(bundle);sendBroadcast(intent);}/*** 按钮:下一首音乐的广播*/public void nextMusic() {Intent intent = new Intent();intent.setAction(MusicService.ACTION);Bundle bundle = new Bundle();bundle.putInt(MusicService.BTN_STATE, MusicService.NEXT_MUSIC_STATE);intent.putExtras(bundle);sendBroadcast(intent);}/*** 按钮:上一首音乐的广播*/public void preMusic() {Intent intent = new Intent();intent.setAction(MusicService.ACTION);Bundle bundle = new Bundle();bundle.putInt(MusicService.BTN_STATE, MusicService.PRE_MUSIC_STATE);intent.putExtras(bundle);sendBroadcast(intent);}}

public class MusicService extends Service {private int mPosition;//静态存储上次音乐条目的位置static int savePosition;private MediaPlayer mMediaPlayer;//静态存储上次音乐条目的MediaPlayerstatic MediaPlayer mPreMediaPlayer;private ArrayList<Music> mMusicList;static boolean isPlaying = true;//动态广播的Actionpublic static String ACTION = "action";//按钮点击状态标识符public static String BTN_STATE = "btn_state";public static final int PLAY_STATE = 0, NEXT_MUSIC_STATE = 1, PRE_MUSIC_STATE = 2;private static final String CHANNEL_ID = "1";private static final String CHANNEL_NAME = "MyChannel";private static final int NOTIFICATION_ID = 2;private RemoteViews remoteViews;private Notification notification;public MusicService() {}//onCreate()只被执行一次,因此用来做初始化@Overridepublic void onCreate() {super.onCreate();mMusicList = MusicList.getMusicData(this);//在MusicService刚刚启动的时候就注册了一个广播,为的是让它来接收到在其他页面点击了上一曲、下一曲、暂停/播放等按钮时,来做相应的处理//因此,其他页面中点击上一曲、下一曲、暂停、播放按钮时都需要向MusicService发送广播通知,让它来更新音乐IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(ACTION);registerReceiver(receiver, intentFilter);}class MusicBinder extends Binder {public MusicService getService() {return MusicService.this;}}@Overridepublic IBinder onBind(Intent intent) {return new MusicBinder();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {mPosition = intent.getExtras().getInt("position", -1);initNotification();if (mPreMediaPlayer == null || mPosition != savePosition) {playMusic(mPosition);} else {mMediaPlayer = mPreMediaPlayer;}return super.onStartCommand(intent, flags, startId);}/*** 初始化Notification(系统默认UI)* 这里是谷歌官方使用步骤:* A:要开始,您需要使用notificationCompat.builder对象设置通知的内容和通道。* B: 在Android 8.0及更高版本上传递通知之前,必须通过将NotificationChannel实例传递给CreateNotificationChannel(),在系统中注册应用程序的通知通道。* C: 每个通知都应该响应tap,通常是为了在应用程序中打开与通知对应的活动。为此,必须指定用PendingIntent对象定义的内容意图,并将其传递给setContentIntent()。* D: 若要显示通知,请调用notificationManagerCompat.notify(),为通知传递唯一的ID以及notificationCompat.builder.build()的结果。*/private void initNotification() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);NotificationManager manager = getSystemService(NotificationManager.class);manager.createNotificationChannel(channel);}Intent intent = new Intent(this, MainActivity.class);PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);remoteViews = new RemoteViews(getPackageName(), R.layout.notification);remoteViews.setTextViewText(R.id.tv_music_name, mMusicList.get(mPosition).getName());remoteViews.setOnClickPendingIntent(R.id.iv_pre_music, getPendingIntent(this, DetailActivity.NOTIFICATION_PRE_MUSIC));remoteViews.setOnClickPendingIntent(R.id.iv_play_pause, getPendingIntent(this, DetailActivity.NOTIFICATION_PLAY));remoteViews.setOnClickPendingIntent(R.id.iv_next_music, getPendingIntent(this, DetailActivity.NOTIFICATION_NEXT_MUSIC));NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);builder.setSmallIcon(R.drawable.music).setContent(remoteViews).setOngoing(true).setContentIntent(pendingIntent);notification = builder.build();//两种方法都可以,这里使用兼容的NotificationManagerNotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(this);notificationManagerCompat.notify(NOTIFICATION_ID, notification);// NotificationManager manager = (NotificationManager) getSystemService(Service.NOTIFICATION_SERVICE);// manager.notify(NOTIFICATION_ID,builder.build());}private PendingIntent getPendingIntent(Context context, int state) {Intent intent = new Intent();intent.setAction(DetailActivity.NOTIFICATION_ACTION);Bundle bundle = new Bundle();bundle.putInt(DetailActivity.NOTIFICATION_BTN_STATE, state);intent.putExtras(bundle);PendingIntent pendingIntent = null;switch (state) {case DetailActivity.NOTIFICATION_PRE_MUSIC:pendingIntent = PendingIntent.getBroadcast(context,1,intent,PendingIntent.FLAG_UPDATE_CURRENT);break;case DetailActivity.NOTIFICATION_PLAY:pendingIntent = PendingIntent.getBroadcast(context,2,intent,PendingIntent.FLAG_UPDATE_CURRENT);break;case DetailActivity.NOTIFICATION_NEXT_MUSIC:pendingIntent = PendingIntent.getBroadcast(context,3,intent,PendingIntent.FLAG_UPDATE_CURRENT);break;}return pendingIntent;}/*** 更新Notification*/public void updateNotification() {remoteViews.setImageViewResource(R.id.iv_play_pause, isPlaying ? R.drawable.pause : R.drawable.play);remoteViews.setTextViewText(R.id.tv_music_name, mMusicList.get(mPosition).getName());NotificationManagerCompat manager = NotificationManagerCompat.from(this);manager.notify(NOTIFICATION_ID, notification);}/*** 广播接收器:接收playMusic()、nextMusic()、preMusic()的动态广播*/private BroadcastReceiver receiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (intent.getAction().equals(ACTION)) {switch (intent.getIntExtra(BTN_STATE, -1)) {case PLAY_STATE:play();break;case PRE_MUSIC_STATE:next(-1);break;case NEXT_MUSIC_STATE:next(1);break;default:Toast.makeText(context, "系统出错,请稍后重试!", Toast.LENGTH_SHORT).show();break;}updateNotification();}}};/*** 播放音乐*/public void playMusic(int position) {mMediaPlayer = new MediaPlayer();if (mPreMediaPlayer != null) {mPreMediaPlayer.stop();mPreMediaPlayer.release();}mPreMediaPlayer = mMediaPlayer;savePosition = position;try {mMediaPlayer.reset();mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());mMediaPlayer.prepare();mMediaPlayer.start();} catch (IOException e) {e.printStackTrace();}mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {mPosition += 1;mPosition = (mMusicList.size() + mPosition) % mMusicList.size();playMusic(mPosition);Toast.makeText(getApplicationContext(), "自动为您切换下一首:" + mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();updateNotification();}});}/*** 按钮点击:播放音乐*/public void play() {if (mMediaPlayer.isPlaying()) {mMediaPlayer.pause();isPlaying = false;} else {mMediaPlayer.start();isPlaying = true;}}/*** 按钮点击:下一首*/public void next(int offset) {mPosition += offset;mPosition = (mMusicList.size() + mPosition) % mMusicList.size();playMusic(mPosition);isPlaying = true;}/*** 设置音乐播放的进度*/public void seekTo(int progress) {mMediaPlayer.seekTo(progress);}/*** 获取当前音乐的名字*/public String getName() {return mMusicList.get(mPosition).getName();}/*** 获取当前音乐的播放时间*/public int getTime() {return mMusicList.get(mPosition).getTime();}/*** 获取当前播放位置*/public int getCurrent() {return mMediaPlayer.getCurrentPosition();}}

这里有个巨坑,就是remoteViews.setOnClickPendingIntent的第二个参数传入的是PendingIntent,因为PendingIntent中需要传入一个Intent,我们就是通过这个Intent来发送按钮的广播。我尝试着用三个不同的Intnet,每一个PendingIntent包含一个Intent,这样三个PendingIntent就不一样了。原理确实是这样的,但是按照我们常规的,对PendingIntent的参数设置就是(this,0,intent,0),发现效果出不来。于是我百度了一些资料,基本都说是第四个参数的原因,于是我将第四个参数改为 PendingIntent.FLAG_UPDATE_CURRENT,发现还是不行。经过一系列的注释、Log,还是没发现问题。最后突然想到,PendingIendingInten的第二个参数我们还没用到,一直传入0是什么原因,于是我试着更改下三个PendingIntent用不同的reqestCode,结果就成了。第二个参数的翻译过来的意思就是请求码,请求码不一样系统才认为是不一样的PendingIntent。

有任何疑问欢迎留言提问。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。