NativeSongActivity
首先是一个viewpage,使用的是public static class MyViewpagerFragment extends ViewPageFragment
public abstract class ViewPageFragment extends Fragment;
而ViewPageFragment持有一个MusicViewPager对象。
通过MyViewpagerFragment#initFragmentList()中初始化了ViewPage需要的fragment。将需要显示的页面的fragment加入MyViewpagerFragment的mFragments中。
还需要在MyViewpagerFragment.getTagText()返回所有viewpage对应的tab字符串。
在MyViewpagerFragment#initFragmentList()中加入了三个fragment。分别是AllSongFragment,SingerFragment,AlbunFragment。
AllSongFragment是NativeSongActivity的内部类。
class AllSongFragment extends NativeListFragment;
class NativeListFragment extends BaseListFragment;
class BaseListFragment extends Fragment;
其中BaseListFragment持有一个protected GnMusicDragListView mListView;是一个ExpandableListView。必然会有一个adapter。
该ListView在右边缘首先会把事件传给子view,如果move事件就接下来的事件由listview自己处理。和listview同一个frameLayout的是AmigoAlphabetIndexView;AmigoAlphabetIndexView覆盖在listview上面。覆盖了整个listview,而不只是看起来的只有一竖条。。
BaseListFragment使用的是tracklist.xml。AmigoAlphabetIndexView的显示和停止由AmigoAlphabetIndexView的onTouchListener和listview的onTouchListener共同完成,
还有其他地方,如切换page时,在void doOnPageChange(int pageNo)中控制。还有在nativesongactivity的onPause()中控制。
SingerFragment和AlbumFragment差不多,我就主要说说SingerFragment。
SingerFragment没有像AllSongFragemtn有那么复杂的继承关系,直接继承Fragment。
下面细看SingerFragment的成员:
AsyncQueryHandler mQueryHandler; 用于查询数据库返回一个Cursor。具体查询的参数等在SingerFragment#getArtistCursor(AsyncQueryHandler async, String filter)中。
Cursor mArtistCursor; 用于保存mQueryHandler返回的cursor。在listView和adapter直接使用这个cursor,而不是用list作为dataset。
ArrayList<String> mArtistList; 用于保存本地没有对应artist的图片的那些Aritist name。本来应该是在联网时用来下载图片的,但是海外是本地版,所以没有下载功能,用到mArtistlist的代码已经裁剪了,但是这个变量还保留着。
boolean mIsBulkOp 用于判断当前是否已经处于批量选择状态了。
List<Integer> mBulkList 用于存储被选中的Artist的id。
View mNoSongZone; 没有歌曲时的页面显示的View。
Bitmap mDefaultBitmap; Artist没有对应图片时使用的默认图片。
LruCache<String, Bitmap> mMemoryCache; 图片缓存工具。缓存artist的圆形图片。缓存在内存中。
关于批量选择过程:
长按就会出发批量选择。
然后会隐藏某些view,显示出某些view,list变得不可点击,并出现了原来gone的小方框。
在每个MyViewpageFragment中的fragment的onLongClickListenter中都会调用NativeSongActivity的doBulkClick(int pos)。在批量选择模式下弹出的收藏、删除、全部选择按钮都是属于NativeSongActivity的,点击这些按钮的话,会调用各自fragment的一些方法(方法名是一样的分别是add和delete开头的方法)。但各个fragment并没有同一个并没有继承同一个接口,这个是不合理的。所以fragment和NativeSongActivity是存在交互的。
在Artist和album这两个页面跳进去分别是SingerActivity和AlbumActivity,都差不多的。
需要传几个参数给Singer Activity,artistName,artistId,分别用于显示、获取对应图片和获取对应该artistId的歌曲。
下面介绍其成员:
SingerPicFragment mFragmentDown; 用于显示上部的图片。
NativeSongListFragment mFragmentUp; 用于显示歌曲列表。
GradientRelativeLayout mBaseLayout; 用于显示虚化背景。 SingerActivity是继承GnMusicLayeredPageActivity。但是虚化背景的layout是在MusicBaseActivity中加入的,就是在musicbaselayout.xml中。虚化背景的设置在SingerActivity.initBlurBg()中。 主要使用了BlurBitmapUtils.getBlurBitmap(bmp1, new BlurBitmapParam(180.0f, 180.0f, 2));将一个普通图片处理了一下。
刪除音乐逻辑
首先startActivityForResult( intent ),启动DeleteItems这个Activity,然后调用DeleteItems#doDelelteItems()方法.
public static void deleteTracks(Context context, long[] list) {
LogUtil.d(TAG, ">> deleteTracks");
String[] cols = new String[] {MusicStore.Audio.Media._ID, MusicStore.Audio.Media.DATA,
MusicStore.Audio.Media.ALBUM_ID};
StringBuilder where = new StringBuilder();
where.append(MusicStore.Audio.Media._ID + " IN (");
for (int i = 0; i < list.length; i++) {
where.append(list[i]);
if (i < list.length - 1) {
where.append(",");
}
}
where.append(")");
// Make sure database exist when deleting and make sure we have latest
// news
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return;
}
Cursor c = CursorUtils.query(context, MusicStore.Audio.Media.EXTERNAL_CONTENT_URI, cols, where.toString(), null,
null);
if (c != null) {
// step 1: remove selected tracks from the current playlist, as well
// as from the album art cache
try {
c.moveToFirst();
while (!c.isAfterLast()) {
// remove from current playlist
long id = c.getLong(0);
sService.removeTrack(id, 1);
c.moveToNext();
}
} catch (Throwable ex) {
Log.i(TAG, "deleteTracks e =" + ex);
}
// step 2: remove selected tracks from the database
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return;
}
try {
context.getContentResolver().delete(MusicStore.Audio.Media.EXTERNAL_CONTENT_URI,
where.toString(), null);
} catch (Exception e) {
LogUtil.d(TAG, "Exception", e);
// Just in case
return;
}
// step 3: remove files from card
c.moveToFirst();
while (!c.isAfterLast()
&& Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String name = c.getString(1);
File f = new File(name);
try { // File.delete can throw a security exception
if(name.startsWith("/system/media/gn_resources/music")){
deleteFileInDB(name,context.getContentResolver());
}
if (!f.delete()) {
// I'm not sure if we'd ever get here (deletion would
// have to fail, but no exception thrown)
LogUtil.e(TAG, "Failed to delete file " + name);
}
c.moveToNext();
} catch (SecurityException ex) {
c.moveToNext();
}
}
// step 4: remove info from recognize table and backup table
try {
c.moveToFirst();
ArrayList<String> paths = new ArrayList<String>();
while (!c.isAfterLast()) {
String path = c.getString(1);
if (!TextUtils.isEmpty(path)) {
paths.add(path);
}
c.moveToNext();
}
} catch (Exception e) {
e.printStackTrace();
}
c.close();
}
// We deleted a number of tracks, which could affect any number of
// things
// in the media content domain, so update everything.
context.getContentResolver().notifyChange(Uri.parse("content://gnmusic"), null);
Bundle args = new Bundle();
args.putString("volume", MusicProvider.EXTERNAL_VOLUME);
Intent intent = new Intent();
intent.setClassName("com.android.providers.media", "com.android.providers.media.MediaScannerService");
intent.putExtras(args);
try {
context.startService(intent);
} catch (Exception e) {
LogUtil.d(TAG, "Exception", e);
}
long minSize = MusicPreference.getMinFilterSize(context);
int n2 = MediaDBUtils.getMusicFileCount(context,minSize);
MusicPreference.setMediaFileCnt(context,n2);
LogUtil.d(TAG, "<< deleteTracks");
}
在这个方法里面,做了五件事:
1.查询数据库时候有mItemList中打songID
2.从PlayQueue中的playList中移除对应的元素,使用PlayQueue#remove(int)
3.从数据库中移除这些uri
4.从存储器中移除这些文件
5.删除对应的备份信息
这个方法并没有在删除后,调用next()或者通知metachange,playqueueChange等信息.
调用完DeleteItems#doDelelteItems()方法后,就会远程调用了MediaPlaybackService的private final IBinder mBinder = new ServiceStub(this)的gotoNextSong()
static class ServiceStub extends IMediaPlaybackService.Stub
public void gotoNextSong() {
if (mService != null && mService.get() != null) {
mService.get().next(true);
}
}
最终调用了MediaPlaybackService#next(boolean).
public void next(boolean force) {
synchronized (this) {
LogUtil.d(TAG, ">> next(" + force + ")");
if (mOnlineMusic) {
if (!NetworkUtil.isNetworkConnected(this)) {
LogUtil.d(TAG, "Network disConnected isPlaying()="+isPlaying());
if (isPlaying()) {
pause(PauseReason.SWICH_SONG);
}
return;
}
}
mPlayQueue.next(isRandPlayMode());
//Gionee <zhangjinbiao> <2017-02-27> modify for <74429> begin
if(0 == mPlayQueue.getQueueSize())
{
LogUtil.i(TAG, "0 == mPlayQueue.getQueueSize");
stop(true);
if (!mOnlineMusic) {
// play();
notifyChange(META_CHANGED);
}
return;
}
//Gionee <zhangjinbiao> <2017-02-27> modify for <74429> begin
stop(false);
openCurrent(true);
// Gionee <liugp><2013-05-22> modify for CR00823332 begin
Intent intent = new Intent("com.android.music.play.next");
sendBroadcast(intent);
// Gionee <liugp><2013-05-22> modify for CR00823332 end
if (!mOnlineMusic) {
play();
notifyChange(META_CHANGED);
} else {
notifyChange(REFRESH_BAR);
updateNotification();
}
}
}
看看 mPlayQueue.next(isRandPlayMode());这一句:
public void next(boolean randomNext){
if(playList == null)
return;
if(randomNext == true){
curPos = getRandomPos();
}else{
if (curPos < playList.size() - 1) {
// 如果不相等,应该是之前做过删除操作
if (getCurSongId() == playList.get(curPos).getSongId()) {
curPos++;
}
} else {
curPos = 0;
}
}
setCurPlayItem(curPos);
LogUtil.i(TAG, "playQueue next() " + "playList size: " + playList.size() + "curPos: " + curPos );
}
好像curPos怎么都不是小于0的.
然后需要在setCurPlayItem()中做处理,
private void setCurPlayItem(int pos){
if(playList != null && playList.size() > 0){
if(pos >= 0 && pos < playList.size()){
curPos = pos;
LogUtil.i(TAG, "setCurPlayItem pos="+curPos);
curPlayItem = playList.get(curPos);
}
}
//Gionee <zhangjinbiao> <2017-2-27> modify for <74429> begin
else {
curPlayItem = null;
}
//Gionee <zhangjinbiao> <2017-2-27> modify for <74429> end
}
在PlayList.size()==0的时候,就让curPlayItem为null,因为其他地方一般会直接操作这个curPlayItem,而不是curPos,如果curPlayItem为null,就会让他们知道没有歌曲.
然后在看看MediaPlaybackService中的这几句
if(0 == mPlayQueue.getQueueSize())
{
LogUtil.i(TAG, "0 == mPlayQueue.getQueueSize");
//用于停掉音乐和取消Service作为前台和让通知栏中打音乐消失
stop(true);
if (!mOnlineMusic) {
// play();
//通知更新
notifyChange(META_CHANGED);
}
return;
}
MusicBaseActivity中有这个监听器
f.addAction(MediaPlaybackService.META_CHANGED);
registerReceiver(mPlayStatusListener, filter);
private BroadcastReceiver mPlayStatusListener = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LogUtil.i(TAG,"mPlayStatusListener mIspaused="+mIspaused);
if(mIspaused == false){
MusicUtils.updateNowPlaying(MusicBaseActivity.this);
}
}
};
updataNowPlaying的代码如下:
就是更新下面那个播放状态栏.
META_CHANGE还会通知到大小Widget,即桌面RemoteView,可以看到,如果curPlayItem为null的话,那么intent中打extra都会为null,因为getArtistName()会
通过curPlayItem去拿.
void notifyChange(String what) {
LogUtil.d(TAG, "notifyChange(" + what + ")");
try {
Intent i = new Intent(what);
i.putExtra("id", Long.valueOf(getAudioId()));
i.putExtra("artist", getArtistName());
i.putExtra("album", getAlbumName());
i.putExtra("track", getTrackName());
i.putExtra("playing", isPlaying());
//Gionee <hongsq> <2015-12-17> adapt for <offline version> begin
i.putExtra("oversea",true);
//Gionee <hongsq> <2015-12-17> adapt for <offline version> end
if (QUIT_PLAYBACK.equals(what)) {
// for QUIT_PLAYBACK, do NOT use sticky broadcast
sendBroadcast(i);
} else {
sendStickyBroadcast(i);
}
if (what.equals(QUEUE_CHANGED)) {
saveQueue(true);
} else {
saveQueue(false);
}
// Share this notification directly with our widgets
mAppWidgetProvider.notifyChange(this, what);
mSmallWidgetProvider.notifyChange(this, what);
} catch (Throwable e) {
// TODO: handle exception
}
}
更新查询
在每次进入NativeSongActivity页面,都会重新刷新页面,就是调用各个fragment的刷新方法.就是在NativeSongActivity#onResume( )中
public void reInitTrackCursorEx() {
if (mAllSongFg != null) {
mAllSongFg.reInitTrackCursor();
}
if (mSingerFg != null) {
mSingerFg.reInitTrackCursor();
}
if(mAlbumFg != null){
mAlbumFg.reInitTrackCursor();
}
}
对于mAllSongFg的话,就是调用NativeListFragment#initTrackCursor( )
protected void initTrackCursor() {
try {
closeCursor();
/*if(mSortOrder == null){
if (mCurrentSourceLocation == null || (!mCurrentSourceLocation
.equals("all") && !mCurrentSourceLocation
.equals("featured"))) {
mSortOrder = "title_key";
}
}*/
if (mBestCollectionsId != null) {
//从我喜欢或这自建歌单进入
if(mBestCollectionsId.equals("-1")) {
//我喜欢歌单
mBestCollectionsId = "" + MusicDBUtils.idForplaylist(mContext, "my_featured");
}else if(mBestCollectionsId.equals("-2")){
//我收藏的歌单
mBestCollectionsId = "" + getPlaylistIdByPlaylistName(mSelectedName);
}
Uri queryUri = Uri.parse("content://gnmusic/external/audio/playlists/" + mBestCollectionsId
+ "/members");
mTrackQueryHandler.startQuery(0, null, queryUri, mBestCollectionCursorCols, mWhere.toString(),
null, mSortOrder);
} else {
//从扫描页面进入
Uri queryUri = Uri.parse("content://gnmusic/external/audio/media");
mTrackQueryHandler
.startQuery(0, null, queryUri, mCursorCols, mWhere.toString(), null, mSortOrder);
}
//Log.d("yangfeng", "nativelist mSortOrder == " + mSortOrder);
} catch (Exception e) {
e.printStackTrace();
Log.i("NativeListFragment", "mCurrentSourceLocation = " + mCurrentSourceLocation);
/*Log.i("NativeListFragment", "mCursorCols = " + mCursorCols.toString() + ", where = " + mWhere
+ ", mSortOrder = " + mSortOrder);*/
Log.i("NativeListFragment", e.toString());
}
}
专辑图片及艺术家图片获取:在MusicUtils中的getLocalAlbums(Context c, long songId),在内存中会有两份图片,只是一张图片,
两个命名而已,但是都是从ALBUM_ARTIST来的
在数据库查询而得到的图片,会缓存到Android/data/com.android.music中.而数据库本身解析的歌曲图片会放在
Android/data/com.android.provider.media/albumthumbs中.
图片获取的逻辑大概是,先看本地有没有缓存,没有就去查数据库.
public static String getLocalAlbums(Context c, long songId) {
String songPath = null;
String[] mCursorCols = new String[] {
MusicStore.Audio.Media.ARTIST, MusicStore.Audio.Media.ALBUM, MusicStore.Audio.Media.TITLE,
MusicStore.Audio.Media.DATA};
Cursor mCursor = CursorUtils.query(c, MusicStore.Audio.Media.EXTERNAL_CONTENT_URI,
mCursorCols, "_id=" + songId, null, null);
if (mCursor != null) {
if(mCursor.getCount() > 0){
mCursor.moveToFirst();
songPath = mCursor.getString(3);
Log.e("getAlbums", "1:songPath:"+songPath);
mCursor.close();
} else {
mCursor.close();
return null;
}
} else {
Log.e("getAlbums", "not find songPath!!!");
return null;
}
String[] idCols = new String[] {
MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ALBUM_ID,MediaStore.Audio.Media.DATA
};
String album=null;
Cursor idCur = null;
idCur = CursorUtils.query(c, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, idCols, MediaStore.Audio.Media.DATA+"=?", new String[]{songPath}, null);
if (idCur == null) {
Log.e("getAlbums", "NOT FIND ALBUM ID!");
return null;
}
if (idCur.getCount() > 0) {
Log.e("getAlbums", "11.COUNT:"+idCur.getCount());
idCur.moveToFirst();
Log.e("getAlbums", "2:ALBUM:"+idCur.getString(0)+",ALBUM_ID:"+idCur.getLong(1));
album = idCur.getString(0);
idCur.close();
} else {
idCur.close();
return null;
}
Uri uri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
String[] cols = new String[] {
MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ALBUM_ART
};
Cursor cur = null;
cur = CursorUtils.query(c,
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, cols, MediaStore.Audio.Albums.ALBUM+"=?", new String[]{album}, null);
if (cur == null) {
Log.e("getAlbums", "CUR IS NULL");
return null;
}
String path = null;
Log.e("getAlbums", "cur.getCount():"+cur.getCount());
if (cur.getCount() > 0) {
cur.moveToFirst();
path = cur.getString(1);
Log.e("getAlbums", "3:ALBUM:"+cur.getString(0)+",ALBUM_ART:"+cur.getString(1));
cur.close();
return path;
} else {
cur.close();
return null;
}
}