Music app框架设计及总结

来源:互联网 发布:10月份经济数据2017 编辑:程序博客网 时间:2024/05/16 13:56

作者 Chaoqian.chen


总体上Music App分为UI界面、服务两个模块,其中关于音乐文件的播放都由服务负责,服务配合AIDL使用的,界面绑定服务后可以拿到服务里所有参数及状态进行UI刷新。

A. 界面模块:
1、主界面MusicMainActivity:


主界面主要负责分类显示音乐文件,以及对音乐文件的各类操作。

MusciAllFragment:显示所有单曲。


SingerFragment:根据歌手分类显示。


AlbumFragment:根据专辑分类显示。

RrecentlyPlayFragment:显示最近播放的歌曲

PlayListFragment:显示用户收藏、录音以及自己创建的播放列表。

2、音乐播放界面MusicPlayingActivity

主要负责展示具体某一首歌曲的详细信息以及播放操作等。


3、音乐搜索界面SearchMusicActivity

输入内容后自动从本地的音乐文件的音乐名,专辑名,歌手名去匹配,匹配后显示到搜索列表里。

4、音乐列表界面MusicListActivity

负责显示播放列表里的歌曲,跟单曲差不多。

5、编辑界面EditMusicActivity

批量编辑音乐文件,包括删除和批量添加到播放列表。

6、PlayingFromUriActivity

负责接收外来资源的播放界面,逻辑跟播放界面一样。


B. 服务模块:


启动主界面后绑定服务,所有界面在onResume里根据服务是否存在判断是否进行绑定,在onStop里根据通知栏是否存在判断是否进行解绑(因为很多时候写在onDestroy里执行不到解绑服务的,导致服务永生不死,不符合谷歌规范)。由于服务绑定的都是单个Activity,若结束当前绑定的Activity,服务则会自动解绑执行onUnbind方法。

为了让服务能一直播放音乐…所以调用服务播放音乐时,就会调用startService为当前服务进行续命,并显示通知栏。所以就算杀掉APP,服务也会继续后台播放,若关闭通知栏则调用stopSelf杀掉服务。若此时点击通知栏调出UI播放界面后(此时的服务是之前续命的服务,并没有绑定任何Activity),再关闭通知栏,则会先stopSelf再发送一个广播通知当前Activity进行重新绑定服务。


C. 具体实现:

进入界面后首先要做的就是扫描本地所有音乐文件:

String[] paths = new String[] {Environment.getExternalStoragePublicDirectory(

                         Environment.DIRECTORY_MUSIC).toString()};

                  // String[] paths = new

                  // String[]{Environment.getExternalStorageDirectory().toString()};

                  MediaScannerConnection.scanFile(c,paths, null, new OnScanCompletedListener() {

                                     @Override

                                     publicvoid onScanCompleted(String path, Uri uri) {

                                               ObservableManager.getInstance().setData(Constants.DATA_CHANGE_DELETE_SONGS);

                                              

                                     }

                            });

 

接着从媒体库拿各个Fragment的数据,如单曲:

Stringwhere = MediaStore.Audio.Media.IS_MUSIC +"=1";

       Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,null, where,

              null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);

 

拿到Cursor后转成你所需要的对象即可展示了:

List<MusicInfo>infos = new ArrayList<MusicInfo>();

       if (cursor ==null) {

           returninfos;

       }

       while (cursor.moveToNext()) {

           MusicInfo info = new MusicInfo();

           // 歌曲IDMediaStore.Audio.Media._ID

           longid;

           if (type == Constants.TYPE_PLAYLIST) {

              id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID));

           } else {

              id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID));

           }

 

           String title, artist;

           // 歌曲文件的路径MediaStore.Audio.Media.DATA

           String url = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));

           String name =cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME));

           // 歌曲的名称MediaStore.Audio.Media.TITLE

           title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));

           // 歌曲的歌手名: MediaStore.Audio.Media.ARTIST

           artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));

           String album = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));

           // 歌曲的总播放时长MediaStore.Audio.Media.DURATION

           longduration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));

           // 歌曲文件的大小MediaStore.Audio.Media.SIZE

           longsize = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));

           longartistsId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));

           longalbumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));

 

           String displayName =cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME));

 

           info.setName(title);

           info.setId(id);

           info.setPath(url);

           info.setArtists(artist);

           info.setAlbum(album);

           info.setArtistsId(artistsId);

           info.setAlbumId(albumId);

           info.setSize(size);

           info.setDuration(duration);

           info.setDisplayName(displayName);

           String tag = PingYinUtil.chineneToSpell(title);

           if (tag.length() < 1) {

              cursor.close();

              returninfos;

           }

           charc = tag.toUpperCase().charAt(0);

           if (!('A' <=c && c <= 'Z')) {

              tag = "#";

           }

           info.setFirstLetter(String.valueOf(tag.charAt(0)).toUpperCase());

           info.setPingYinName(tag);

           infos.add(info);

 

       }

       cursor.close();

其他的就不一一列出来了。

数据UI都有了,接下来就要开始创建服务准备播放了,先在服务里封装好一个播放器并与AIDL关联好:

private class MultiPlayer {

       private MediaPlayermCurrentMediaPlayer = new MediaPlayer();

       private MediaPlayermNextMediaPlayer;

       private HandlermHandler;

       private boolean mIsInitialized = false;

 

       public MultiPlayer() {

           mCurrentMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);

       }

 

       public void setDataSource(String path) {

           mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer,path);

           Log.i(TAG,"setDataSource() mIsInitialized :" + mIsInitialized);

           if (mIsInitialized) {

              setNextDataSource(null);

           }

       }


       private boolean setDataSourceImpl(MediaPlayer player, String path) {

           try {

              Log.d(TAG,"setDataSourceImpl() player : " + player + ",path : " + path + ",cursor: " + mCursor);

              if (mCursor ==null) {

                  returnfalse;

              }

              player.reset();

              if (path.startsWith("content://")) {

                  player.setDataSource(MediaPlaybackService.this, Uri.parse(path));

              } else {

                  player.setDataSource(path);

              }

              player.setAudioStreamType(AudioManager.STREAM_MUSIC);

              player.prepare();

              Log.i(TAG,"setDataSourceImpl() afterprepare()");

           } catch (IOExceptionex) {

              // TODO: notify the user why the file couldn't beopened

              return false;

           } catch (IllegalArgumentExceptionex) {

              // TODO: notify the user why the file couldn't beopened

              return false;

           }

           player.setOnCompletionListener(listener);

           player.setOnErrorListener(errorListener);

           player.setOnPreparedListener(new OnPreparedListener() {

 

              @Override

              public void onPrepared(MediaPlayer mp) {

                  // TODO Auto-generated method stub

                  // mp.start();

              }

           });

           return true;

       }

 

       public void setNextDataSource(String path) {

           Log.d(TAG,"setNextDataSource() enter path :" + path + ",mNextMediaPlayer : " + mNextMediaPlayer);

           if (mNextMediaPlayer !=null) {

              mNextMediaPlayer.release();

              mNextMediaPlayer =null;

              mCurrentMediaPlayer.setNextMediaPlayer(null);

           }

           if (path ==null) {

              return;

           }

           mNextMediaPlayer = new MediaPlayer();

           mNextMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);

           mNextMediaPlayer.setAudioSessionId(getAudioSessionId());

           if (setDataSourceImpl(mNextMediaPlayer,path)) {

              mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);

           } else {

              // failed to open next, we'll transitionthe old fashioned way,

              // which will skip over the faulty file

              mNextMediaPlayer.release();

              mNextMediaPlayer =null;

           }

       }

 

       public boolean isInitialized() {

           returnmIsInitialized;

       }

 

       public void start() {

           MusicUtils.debugLog(new Exception("MultiPlayer.start called"));

           mCurrentMediaPlayer.start();

       }

 

       public void stop() {

           mCurrentMediaPlayer.reset();

           mIsInitialized = false;

       }

 

       /**

        *You CANNOT use this player anymore after calling release()

        */

       public void release() {

           stop();

           mCurrentMediaPlayer.release();

       }

 

       public void pause() {

           mCurrentMediaPlayer.pause();

       }

 

       public void setHandler(Handler handler) {

           mHandler = handler;

       }

 

       MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {

           public void onCompletion(MediaPlayer mp) {

              Log.d(TAG,"onCompletion : " + (mp ==mCurrentMediaPlayer && mNextMediaPlayer != null)

                     + ",mRepeatMode : " +mRepeatMode);

              if (mRepeatMode !=REPEAT_CURRENT && !mCurrentDataIsremove) {

                  // mCurrentMediaPlayer.release();

                  setNextTrack();

                  mCurrentMediaPlayer =mNextMediaPlayer;

                  mNextMediaPlayer =null;

                  mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);

              } else {

                  mWakeLock.acquire(30000);

                  mHandler.sendEmptyMessage(TRACK_ENDED);

                  mHandler.sendEmptyMessage(RELEASE_WAKELOCK);

              }

           }

       };

 

       MediaPlayer.OnErrorListener errorListener =new MediaPlayer.OnErrorListener() {

           public boolean onError(MediaPlayer mp,int what, int extra) {

              Log.e(TAG,"MediaPlayer.onError() what: " + what + "," + extra);

              switch (what) {

              case MediaPlayer.MEDIA_ERROR_SERVER_DIED:

                  returntrue;

              case -38:

                  if (mPlayList !=null && mPlayListLen <= 1) {

                     MediaPlaybackService.this.stop(true);

                     stopForeground(true);

                  }

                  break;

              default:

                  playSongFail(mp);

                  break;

              }

              return true;

           }

       };

 

播放之前要先准备好待播放文件:

    public boolean open(String path) {

       Log.d(TAG,"open() path : " + path);

       synchronized (this) {

           if (path ==null) {

              return false;

           }

 

           // if mCursor is null, try to associatepath with a database cursor

           if (mCursor ==null) {

 

              ContentResolver resolver = getContentResolver();

              Uri uri;

              String where;

              String selectionArgs[];

              if (path.startsWith("content://media/")) {

                  uri = Uri.parse(path);

                  where = null;

                  selectionArgs =null;

              } else {

                  uri = MediaStore.Audio.Media.getContentUriForPath(path);

                  where = MediaStore.Audio.Media.DATA +"=?";

                  selectionArgs =new String[] { path };

              }

 

              try {

                  mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);

                  if (mCursor !=null) {

                     if (mCursor.getCount() == 0) {

                         mCursor.close();

                         mCursor =null;

                     } else {

                         mCursor.moveToNext();

                         ensurePlayListCapacity(1);

                         mPlayListLen = 1;

                         mPlayList[0] =mCursor.getLong(IDCOLIDX);

                         mPlayPos = 0;

                     }

                  }

              } catch (UnsupportedOperationExceptionex) {

                  Log.d(TAG,"UnsupportedOperationException");

              }

           }

           mFileToPlay = path;

           mPlayer.setDataSource(mFileToPlay);

           if (mPlayer.isInitialized()) {

              mOpenFailedCounter = 0;

              return true;

           }

           stop(true);

           return false;

       }

    }

到这里差不多就可以调用mPlayer.start()播放音乐了。

接着再说下Service的绑定跟解绑的事,服务若直接跟applicationContext绑定,你会发现你的服务就算执行onUnbind,但它还是没死,所以,最好还是选择跟Activity绑定:

    @Override

    protected void onResume() {

       // TODOAuto-generated method stub

        super.onResume();

       if (MusicApplication.getmToken() ==null && !(BaseActivity.thisinstanceof MusicMainActivity)) {

           MusicApplication.setmToken(MusicUtils.bindToService(this,mServiceConnection));

       } else {

           new Handler().postDelayed(new Runnable() {

              public void run() {

                  if (MusicApplication.getmToken() ==null && !(BaseActivity.thisinstanceof MusicMainActivity)) {

                     MusicApplication.setmToken(MusicUtils.bindToService(BaseActivity.this,mServiceConnection));

                  }

              }

           }, 400);

       }

    }

 

    @Override

    protected void onStop() {

       // TODOAuto-generated method stub

       super.onStop();

       if (!MusicApplication.isNotifacationExist()&& MusicUtils.sService !=null

              && MusicUtils.isApplicationBroughtToBackground(getApplicationContext())){

           MusicUtils.unbindFromService(MusicApplication.getmToken());

       }

    }

 

但这样做的唯一缺点就是,只要绑定的Activity结束掉,服务就自动执行了onUnbind。所以只要一播放音乐你可以先弹出通知栏:   

private void updateNotification(Contextcontext, Bitmap bitmap) {

       Log.d(TAG,"updateNotification");

       MusicApplication.setNotifacationExist(true);

       RemoteViews views = new RemoteViews(getPackageName(), R.layout.messagecenter_contralbar);

       String trackinfo = getTrackName();

       String artist = getArtistName();

       if (artist ==null || artist.equals(MediaStore.UNKNOWN_STRING)) {

           artist = getString(R.string.unknown_artist_name);

       }

      

       trackinfo += " -" + artist;

       views.setTextViewText(R.id.txt_trackinfo,trackinfo);

       Intent intent;

       PendingIntent pIntent;

 

       intent = new Intent("com.android.music.PLAYBACK_VIEWER");

        intent.setPackage(getPackageName());

       pIntent = PendingIntent.getActivity(context, 0,intent, 0);

       views.setOnClickPendingIntent(R.id.rl_newstatus,pIntent);

 

       intent = new Intent(PREVIOUS_ACTION);

       intent.setClass(context, MediaPlaybackService.class);

       pIntent = PendingIntent.getService(context, 0,intent, 0);

       views.setOnClickPendingIntent(R.id.btn_prev,pIntent);

 

       intent = new Intent(NOTIFICATION_PAUSE_PLAY_ACTION);

       intent.setClass(context, MediaPlaybackService.class);

       pIntent = PendingIntent.getService(context, 0,intent, 0);

       views.setOnClickPendingIntent(R.id.btn_pause,pIntent);

 

       if (isPlaying()) {

           views.setImageViewResource(R.id.btn_pause, R.drawable.music_message_stop);

       } else {

           views.setImageViewResource(R.id.btn_pause, R.drawable.music_message_play);

       }

 

       intent = new Intent(NEXT_ACTION);

       intent.setClass(context, MediaPlaybackService.class);

       pIntent = PendingIntent.getService(context, 0,intent, 0);

       views.setOnClickPendingIntent(R.id.btn_next,pIntent);

 

       intent = new Intent(NOTIFICATION_STOP_ACTION);

       intent.setClass(context, MediaPlaybackService.class);

       pIntent = PendingIntent.getService(context, 0,intent, 0);

       views.setOnClickPendingIntent(R.id.btn_close,pIntent);

       if (bitmap !=null) {

           views.setImageViewBitmap(R.id.iv_cover,bitmap);

 

       }

       Notification status = new Notification();

       status.contentView =views;

       status.flags |= Notification.FLAG_ONGOING_EVENT;

       status.icon = R.drawable.icon_notify_musicplayer;

       status.contentIntent = PendingIntent.getService(context, 0,intent, 0);

       startForeground(PLAYBACKSERVICE_STATUS,status);

 

    }

并且调用

startService(new Intent(this, MediaPlaybackService.class));为绑定的服务续命,这样就算Activity挂掉后,服务照样能继续播放音乐,如果你想结束掉续命的服务,就只要调用MediaPlaybackService.this.stopSelf();就好了。这样服务就不管怎么样都会执行onDestroy来释放资源了。

 

D.总结

音乐的核心就在于服务,最近遇到的问题基本上都与服务有关。接手之前,服务是永远存在的,这是不符合谷歌规范的,长时间空闲的服务将使所在进程一直处在B Services(oom_adj=8),进程不容易被杀掉、内存较难及时释放。所以尝试着改动。之前的是每个界面都去绑定,为了简化逻辑及代码,对整个服务进行了统一(如上图)。该绑定的时候绑定,该解绑的时候解绑,改释放的资源及时释放。由于服务贯穿整个音乐,所以每次改动后必须每个逻辑都要测试一遍,否则就会出现很多BUG了。

0 0
原创粉丝点击