diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26bae0b..8d9d4c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,6 +78,7 @@ dependencies { implementation("androidx.media3:media3-exoplayer-dash:1.4.1") implementation("androidx.media3:media3-session:1.4.1") implementation("androidx.media3:media3-ui:1.4.1") + implementation ("androidx.media3:media3-database:1.4.1") //----------media3 diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json index e08abd4..6018d3a 100644 --- a/app/objectbox-models/default.json +++ b/app/objectbox-models/default.json @@ -46,9 +46,58 @@ } ], "relations": [] + }, + { + "id": "2:2417298624721077393", + "lastPropertyId": "8:376288481537034221", + "name": "BoxDownloadSong", + "properties": [ + { + "id": "1:7539246810224330948", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:4996334478861162671", + "name": "songName", + "type": 9 + }, + { + "id": "3:7837922119072734767", + "name": "singerName", + "type": 9 + }, + { + "id": "4:102902831836074640", + "name": "videoId", + "type": 9 + }, + { + "id": "5:230178850883428921", + "name": "covert", + "type": 9 + }, + { + "id": "6:5148334492661309747", + "name": "durationMs", + "type": 6 + }, + { + "id": "7:9082762505752465280", + "name": "duration", + "type": 9 + }, + { + "id": "8:376288481537034221", + "name": "cachePath", + "type": 9 + } + ], + "relations": [] } ], - "lastEntityId": "1:7260744904384376845", + "lastEntityId": "2:2417298624721077393", "lastIndexId": "0:0", "lastRelationId": "0:0", "lastSequenceId": "0:0", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 523258f..5ddf8e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + + + + + + + { + + @Override + protected ItemLikeSongBinding getViewBinding(ViewGroup parent) { + return ItemLikeSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + VHolder itemHolder = (VHolder) holder; + ItemLikeSongBinding vb = itemHolder.getVb(); + Download download = data.get(position); + BoxDownloadSong boxDownloadSong = CommonUtils.downloadToBean(download); + + + Glide.with(MusicApplication.myApplication) + .asDrawable() + .load(boxDownloadSong.getCovert()) + .apply(RequestOptions.bitmapTransform(new RoundedCorners(CommonUtils.dpToPx(4)))) + .placeholder(R.drawable.placeholder) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) { + CommonUtils.LogMsg(e.getMessage()); + return false; + } + + @Override + public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { + return false; + } + }) + .into(vb.imCovert); + vb.tvTitle.setText(boxDownloadSong.getSongName()); + vb.tvSingerName.setText(boxDownloadSong.getSingerName()); + + vb.getRoot().setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(homeItemClickListener!= null){ + int absoluteAdapterPosition = itemHolder.getAbsoluteAdapterPosition(); + homeItemClickListener.onClickDownloadSong(download,absoluteAdapterPosition); + } + } + }); + + } + + +} diff --git a/app/src/main/java/com/hi/music/player/api/HomeItemClickListener.java b/app/src/main/java/com/hi/music/player/api/HomeItemClickListener.java index 242b5bf..5daaed0 100644 --- a/app/src/main/java/com/hi/music/player/api/HomeItemClickListener.java +++ b/app/src/main/java/com/hi/music/player/api/HomeItemClickListener.java @@ -1,5 +1,7 @@ package com.hi.music.player.api; +import androidx.media3.exoplayer.offline.Download; + import com.hi.music.player.javabean.BoxLikeSong; import com.hi.music.player.javabean.response.ResponsePlayListInfo; import com.hi.music.player.javabean.response.ResponseSearch; @@ -68,8 +70,22 @@ public interface HomeItemClickListener { } + /** + * 点击喜爱歌曲列表的某一首歌曲 + * @param boxLikeSong + * @param index + */ default void onClickLikeSong(BoxLikeSong boxLikeSong, int index){ } + + /** + * 点击下载歌曲列表的某一首歌曲 + * @param index + */ + default void onClickDownloadSong(Download download, int index){ + + } + } diff --git a/app/src/main/java/com/hi/music/player/helper/CommonUtils.java b/app/src/main/java/com/hi/music/player/helper/CommonUtils.java index ee6e802..517e1dc 100644 --- a/app/src/main/java/com/hi/music/player/helper/CommonUtils.java +++ b/app/src/main/java/com/hi/music/player/helper/CommonUtils.java @@ -13,17 +13,23 @@ import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; +import androidx.annotation.OptIn; import androidx.media3.common.MediaItem; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.Download; import androidx.palette.graphics.Palette; +import com.google.gson.Gson; import com.hi.music.player.MusicApplication; import com.hi.music.player.R; import com.hi.music.player.api.onImageColorListener; +import com.hi.music.player.javabean.BoxDownloadSong; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -84,18 +90,18 @@ public class CommonUtils { //time 3:45 转换成毫秒 public static long convertToMilliseconds(String time) { String[] parts = time.split(":"); - if(parts.length == 2){ + if (parts.length == 2) { //3:45 int minutes = Integer.parseInt(parts[0]); int seconds = Integer.parseInt(parts[1]); - return (minutes * 60L + seconds) * 1000-1000; - }else{ + return (minutes * 60L + seconds) * 1000 - 1000; + } else { //3:45:07 int hours = Integer.parseInt(parts[0]); int minutes = Integer.parseInt(parts[1]); int seconds = Integer.parseInt(parts[2]); - return (hours*60L*60L+minutes * 60L + seconds) * 1000-1000; + return (hours * 60L * 60L + minutes * 60L + seconds) * 1000 - 1000; } @@ -111,9 +117,9 @@ public class CommonUtils { // long seconds = (millis / 1000) % 60; // long minutes = (millis / (1000 * 60)) % 60; - if(hours>0){ - return String.format(MusicApplication.myApplication.getString(R.string.time_format),hours, minutes, remainingSeconds); // 格式化为 mm:ss - }else { + if (hours > 0) { + return String.format(MusicApplication.myApplication.getString(R.string.time_format), hours, minutes, remainingSeconds); // 格式化为 mm:ss + } else { return String.format(MusicApplication.myApplication.getString(R.string.minute_time_format), minutes, remainingSeconds); // 格式化为 mm:ss } @@ -174,7 +180,7 @@ public class CommonUtils { } - public static void getMainColor(Drawable imDraw, onImageColorListener listener){ + public static void getMainColor(Drawable imDraw, onImageColorListener listener) { BitmapDrawable drawable = (BitmapDrawable) imDraw; // 使用 Palette 提取颜色 @@ -188,7 +194,7 @@ public class CommonUtils { if (dominantSwatch != null) { int dominantColor = dominantSwatch.getRgb(); // 获取 RGB 颜色 listener.onImageColor(dominantColor); - }else { + } else { listener.onImageColor(-1); } } @@ -217,6 +223,7 @@ public class CommonUtils { drawable.setCornerRadii(radii); return drawable; } + public static void extractColorsFromImage(Drawable drawable, View view) { Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); @@ -227,34 +234,33 @@ public class CommonUtils { int mutedColor = palette.getMutedColor(0); // 创建左右渐变色 - createGradientBackground(vibrantColor, mutedColor,view); + createGradientBackground(vibrantColor, mutedColor, view); } }); } private static void createGradientBackground(int leftColor, int rightColor, View view) { - GradientDrawable gradientDrawable = new android.graphics.drawable.GradientDrawable( + GradientDrawable gradientDrawable = new android.graphics.drawable.GradientDrawable( android.graphics.drawable.GradientDrawable.Orientation.LEFT_RIGHT, new int[]{leftColor, rightColor} ); - - view.setBackground(gradientDrawable); } /** * 当前媒体项是否有有效的播放地址 + * * @param mediaItem * @return */ public static boolean hasValidUri(MediaItem mediaItem) { MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration; - if(localConfiguration != null){ - if(CommonUtils.isUriFormat(localConfiguration.uri)){ + if (localConfiguration != null) { + if (CommonUtils.isUriFormat(localConfiguration.uri)) { return true; } } @@ -268,9 +274,18 @@ public class CommonUtils { return false; } } catch (Exception e) { - LogMsg( "URI parsing failed: " + e.getMessage()); + LogMsg("URI parsing failed: " + e.getMessage()); return false; } return true; } + + + public static BoxDownloadSong downloadToBean(Download download) { + @OptIn(markerClass = UnstableApi.class) byte[] data = download.request.data; + String additionalData = new String(data, StandardCharsets.UTF_8); + Gson gson = new Gson(); + + return gson.fromJson(additionalData, BoxDownloadSong.class); + } } diff --git a/app/src/main/java/com/hi/music/player/helper/MyValue.java b/app/src/main/java/com/hi/music/player/helper/MyValue.java index 9b66f5d..3bd07a4 100644 --- a/app/src/main/java/com/hi/music/player/helper/MyValue.java +++ b/app/src/main/java/com/hi/music/player/helper/MyValue.java @@ -18,10 +18,11 @@ public class MyValue { //专辑 public static final String PAGE_TYPE_ALBUM="MUSIC_PAGE_TYPE_ALBUM"; + + + //-----------------------------PlayActivity - - /** * 歌手单曲进入的数据key */ @@ -91,7 +92,6 @@ public class MyValue { public final static int TYPE_ENTER_LIKE= 4; - //-----------------------------PlayActivity @@ -105,11 +105,23 @@ public class MyValue { public static String KEY_CATEGORY_LIST_BROWSER_ID= "browser_id"; - //-----------------------------CategoryListActivity + //------------------------ResultListActivity public static String KEY_SEARCH_RESULT_BROWSER_ID= "search_result_browser_id"; - //-------------------------------------ResultListActivity + + + + + + + + //---------------------LikeSongActivity + public static String KEY_ENTER_LIKE_ACTIVITY_TYPE = "song_type"; + + public final static int KEY_ENTER_LIKE_ACTIVITY_TYPE_LIKE = 0; + public final static int KEY_ENTER_LIKE_ACTIVITY_TYPE_DOWNLOAD = 1; + } diff --git a/app/src/main/java/com/hi/music/player/javabean/BoxDownloadSong.java b/app/src/main/java/com/hi/music/player/javabean/BoxDownloadSong.java new file mode 100644 index 0000000..e6c5872 --- /dev/null +++ b/app/src/main/java/com/hi/music/player/javabean/BoxDownloadSong.java @@ -0,0 +1,88 @@ +package com.hi.music.player.javabean; + +import io.objectbox.annotation.Entity; +import io.objectbox.annotation.Id; + +@Entity +public class BoxDownloadSong { + @Id + public long id; + private String songName; + + private String singerName; + private String videoId; + + private String covert; + private long durationMs; + private String duration ; + + //下载的音视频文件存储路径 + private String cachePath ; + + public BoxDownloadSong( ) { + + } + + public BoxDownloadSong(String songName, String singerName, String videoId, String covert, long durationMs, String duration) { + this.songName = songName; + this.singerName = singerName; + this.videoId = videoId; + this.covert = covert; + this.durationMs = durationMs; + this.duration = duration; + } + + public String getCachePath() { + return cachePath; + } + + public void setCachePath(String cachePath) { + this.cachePath = cachePath; + } + + public long getDurationMs() { + return durationMs; + } + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + } + public void setDurationMs(long durationMs) { + this.durationMs = durationMs; + } + + public String getSingerName() { + return singerName; + } + + public void setSingerName(String singerName) { + this.singerName = singerName; + } + + public String getVideoId() { + return videoId; + } + + public void setVideoId(String videoId) { + this.videoId = videoId; + } + + public String getCovert() { + return covert; + } + + public void setCovert(String covert) { + this.covert = covert; + } + + public String getSongName() { + return songName; + } + + public void setSongName(String songName) { + this.songName = songName; + } +} diff --git a/app/src/main/java/com/hi/music/player/media3/MyCacheManager.java b/app/src/main/java/com/hi/music/player/media3/MyCacheManager.java new file mode 100644 index 0000000..5eeadce --- /dev/null +++ b/app/src/main/java/com/hi/music/player/media3/MyCacheManager.java @@ -0,0 +1,32 @@ +package com.hi.music.player.media3; + +import android.content.Context; + +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.cache.NoOpCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; + +import com.hi.music.player.MusicApplication; + +import java.io.File; + +public class MyCacheManager { + + private static SimpleCache downloadCache; + + @OptIn(markerClass = UnstableApi.class) + public static SimpleCache getMyCache(StandaloneDatabaseProvider databaseProvider){ + if(downloadCache == null){ + Context myApplication = MusicApplication.myApplication; + File musicApp = new File(myApplication.getCacheDir(), "Music_App"); + downloadCache = new SimpleCache(musicApp, new NoOpCacheEvictor(), databaseProvider); + } + return downloadCache; + + } + + + +} diff --git a/app/src/main/java/com/hi/music/player/media3/MyDownloadService.java b/app/src/main/java/com/hi/music/player/media3/MyDownloadService.java new file mode 100644 index 0000000..accba57 --- /dev/null +++ b/app/src/main/java/com/hi/music/player/media3/MyDownloadService.java @@ -0,0 +1,210 @@ +package com.hi.music.player.media3; + +import android.app.Notification; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.DefaultHttpDataSource; +import androidx.media3.datasource.cache.SimpleCache; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadCursor; +import androidx.media3.exoplayer.offline.DownloadIndex; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadNotificationHelper; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.exoplayer.scheduler.Scheduler; + +import com.google.gson.Gson; +import com.hi.music.player.MusicApplication; +import com.hi.music.player.R; +import com.hi.music.player.helper.CommonUtils; +import com.hi.music.player.javabean.BoxDownloadSong; +import com.hi.music.player.ui.activity.viewmodel.VMApplication; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + + +@UnstableApi +public class MyDownloadService extends DownloadService { + private static final int FOREGROUND_NOTIFICATION_ID = 1; + private static final String CHANNEL_ID = "download_song_channel_id"; + + private static DownloadManager mDownloadManager; + + public MyDownloadService() { + super(FOREGROUND_NOTIFICATION_ID, + DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, + CHANNEL_ID, // 通知渠道 ID + R.string.app_name, // 通知渠道名称 + 0 + ); + } + + public static void init(Context context) { + if (mDownloadManager == null) { + StandaloneDatabaseProvider databaseProvider = new StandaloneDatabaseProvider(context); + SimpleCache downloadCache = MyCacheManager.getMyCache(databaseProvider); + DefaultHttpDataSource.Factory factory = new DefaultHttpDataSource.Factory(); + Executor downloadExecutor = Runnable::run; + + mDownloadManager = new DownloadManager( + context, + databaseProvider, // 数据库提供者 + downloadCache, + factory, // 数据源工厂 + downloadExecutor // 线程池 + ); + + mDownloadManager.setMaxParallelDownloads(3); // 设置最大并行下载数 + + } + } + + public static synchronized DownloadManager getMyDownloadManager() { + + return mDownloadManager; + } + + public static void addDownloadListener(VMApplication vmApplication) { + mDownloadManager.addListener(new DownloadManager.Listener() { + @Override + public void onDownloadChanged(@NonNull DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + String id = download.request.id; + if (download.state == Download.STATE_COMPLETED) { + // 下载完成 + CommonUtils.LogMsg("----------------下载完成 id=" + id + "--thread=" + Thread.currentThread().getName()); + updateDownloadUi(vmApplication); + + } else if (download.state == Download.STATE_FAILED) { + // 下载失败 + CommonUtils.LogMsg("----------------下载失败 id=" + id + "---finalException=" + finalException.getMessage()); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + String id = download.request.id; + CommonUtils.LogMsg("----------------onDownloadRemoved id=" + id); + } + }); + } + + + @UnstableApi + public static void updateDownloadUi(VMApplication vmApplication) { + Disposable subscribe = Observable.fromCallable(() -> { + // 第一部分操作:在IO线程 + DownloadIndex downloadIndex = mDownloadManager.getDownloadIndex(); + DownloadCursor downloads = downloadIndex.getDownloads(Download.STATE_COMPLETED); + + return downloads; + }) + .subscribeOn(Schedulers.io()) +// .observeOn(Schedulers.computation()) // 切换到计算线程 +// .map(result -> { +// // 第二部分操作:在computation线程 +// return result + " -> Step 2 completed"; +// }) + .observeOn(AndroidSchedulers.mainThread()) // 切换到主线程 + .subscribe(finalResult -> { + // 最终结果处理:在主线程 + Download curDownload = null; +// int count = finalResult.getCount(); +// if (finalResult.moveToLast()) { +// curDownload = finalResult.getDownload(); +// } + List downloadList = new ArrayList<>(); + while (finalResult.moveToNext()) { + Download download = finalResult.getDownload(); + downloadList.add(download); + } + + + CommonUtils.LogMsg("----------------下载总数量 -count=" + downloadList.size()); + vmApplication.setDownloadData(downloadList); + + }); + } + + @NonNull + @Override + protected DownloadManager getDownloadManager() { + // 初始化下载管理器 +// StandaloneDatabaseProvider databaseProvider = new StandaloneDatabaseProvider(this); +// SimpleCache downloadCache = MyCacheManager.getMyCache(databaseProvider); +// DefaultHttpDataSource.Factory factory = new DefaultHttpDataSource.Factory(); +// Executor downloadExecutor = Runnable::run; +// +// downloadManager = new DownloadManager( +// this, +// databaseProvider, // 数据库提供者 +// downloadCache, +// factory, // 数据源工厂 +// downloadExecutor // 线程池 +// ); +// +// downloadManager.setMaxParallelDownloads(3); // 设置最大并行下载数 +// downloadManager.addListener(new DownloadManager.Listener() { +// @Override +// public void onDownloadChanged(DownloadManager downloadManager, Download download, @Nullable Exception finalException) { +// String id = download.request.id; +// if (download.state == Download.STATE_COMPLETED) { +// // 下载完成 +// CommonUtils.LogMsg("----------------下载完成 id="+id); +// byte[] data = download.request.data; +// String additionalData = new String(data, StandardCharsets.UTF_8); +// Gson gson = new Gson(); +// BoxDownloadSong boxDownloadSong = gson.fromJson(additionalData, BoxDownloadSong.class); +// +// +// +// } else if (download.state == Download.STATE_FAILED) { +// // 下载失败 +// CommonUtils.LogMsg("----------------下载失败 id="+id+"---finalException="+finalException.getMessage()); +// } +// } +// +// @Override +// public void onDownloadRemoved(DownloadManager downloadManager, Download download) { +// String id = download.request.id; +// CommonUtils.LogMsg("----------------onDownloadRemoved id="+id); +// } +// }); + return getMyDownloadManager(); + } + + + @Nullable + @Override + protected Scheduler getScheduler() { + // 返回 null 表示不需要使用调度器 + return null; + } + + @Override + protected Notification getForegroundNotification(List downloads, int notMetRequirements) { + // 构建用于显示下载进度的通知 + return buildNotification(downloads, notMetRequirements); + } + + private Notification buildNotification( + List downloads, int notMetRequirements) { + + return new DownloadNotificationHelper(this, CHANNEL_ID) + .buildProgressNotification(this, R.drawable.ic_download, null, null, downloads, notMetRequirements); + } + + +} diff --git a/app/src/main/java/com/hi/music/player/media3/PlaybackService.java b/app/src/main/java/com/hi/music/player/media3/PlaybackService.java index 5321da7..78eeae1 100644 --- a/app/src/main/java/com/hi/music/player/media3/PlaybackService.java +++ b/app/src/main/java/com/hi/music/player/media3/PlaybackService.java @@ -48,7 +48,7 @@ public class PlaybackService extends MediaSessionService { long maxCacheSize = 100 * 1024 * 1024; // 缓存大小 100MB StandaloneDatabaseProvider databaseProvider = new StandaloneDatabaseProvider(this); SimpleCache cache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(maxCacheSize), databaseProvider); - + SimpleCache downloadCache = MyCacheManager.getMyCache(databaseProvider); DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true); diff --git a/app/src/main/java/com/hi/music/player/ui/activity/LikeSongActivity.java b/app/src/main/java/com/hi/music/player/ui/activity/LikeSongActivity.java index d2985c3..e438af7 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/LikeSongActivity.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/LikeSongActivity.java @@ -3,9 +3,15 @@ package com.hi.music.player.ui.activity; import android.content.Intent; import android.view.View; +import androidx.annotation.OptIn; +import androidx.lifecycle.Observer; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadManager; import androidx.recyclerview.widget.LinearLayoutManager; import com.hi.music.player.R; +import com.hi.music.player.adapter.AdapterDownloadSong; import com.hi.music.player.adapter.AdapterLikeSong; import com.hi.music.player.api.HomeItemClickListener; import com.hi.music.player.databinding.ActivityLikeSongBinding; @@ -13,6 +19,7 @@ import com.hi.music.player.helper.CommonUtils; import com.hi.music.player.helper.MyValue; import com.hi.music.player.javabean.BoxLikeSong; import com.hi.music.player.javabean.response.ResponsePlayListInfo; +import com.hi.music.player.media3.MyDownloadService; import com.hi.music.player.objectbox.ObjectBoxManager; import java.util.ArrayList; @@ -22,20 +29,48 @@ public class LikeSongActivity extends BaseActivity impl private List boxLikeSongs; + private int mType; @Override protected ActivityLikeSongBinding getViewBinding() { return ActivityLikeSongBinding.inflate(getLayoutInflater()); } + @OptIn(markerClass = UnstableApi.class) @Override protected void onCreateInit() { - boxLikeSongs = ObjectBoxManager.queryAllLike(); - AdapterLikeSong adapterLikeSong = new AdapterLikeSong(); - adapterLikeSong.setHomeItemClickListener(this); - adapterLikeSong.setData(boxLikeSongs); - vb.recycler.setLayoutManager(new LinearLayoutManager(this)); - vb.recycler.setAdapter(adapterLikeSong); - vb.tvSongSize.setText(String.format(getString(R.string.like_song), boxLikeSongs.size())); + Intent intent = getIntent(); + mType = intent.getIntExtra(MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE, MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE_LIKE); + + switch (mType){ + case MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE_LIKE: + vb.tvTitle.setText(getString(R.string.text_like_song)); + boxLikeSongs = ObjectBoxManager.queryAllLike(); + AdapterLikeSong adapterLikeSong = new AdapterLikeSong(); + adapterLikeSong.setHomeItemClickListener(this); + adapterLikeSong.setData(boxLikeSongs); + vb.recycler.setLayoutManager(new LinearLayoutManager(this)); + vb.recycler.setAdapter(adapterLikeSong); + vb.tvSongSize.setText(String.format(getString(R.string.like_song), boxLikeSongs.size())); + break; + + case MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE_DOWNLOAD: + vb.tvTitle.setText(getString(R.string.text_offline_song)); + MyDownloadService.updateDownloadUi(vmApplication); + vmApplication.downloadData.observe(this, new Observer>() { + @Override + public void onChanged(List downloads) { + AdapterDownloadSong adapterDownloadSong = new AdapterDownloadSong(); + adapterDownloadSong.setHomeItemClickListener(LikeSongActivity.this); + adapterDownloadSong.setData(downloads); + vb.recycler.setLayoutManager(new LinearLayoutManager(LikeSongActivity.this)); + vb.recycler.setAdapter(adapterDownloadSong); + vb.tvSongSize.setText(String.format(getString(R.string.download_song), downloads.size())); + } + }); + break; + } + + } @Override @@ -91,5 +126,11 @@ public class LikeSongActivity extends BaseActivity impl + } + + @Override + public void onClickDownloadSong(Download download, int index) { + + } } \ No newline at end of file diff --git a/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java b/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java index 4d18b30..8694c67 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java @@ -20,6 +20,10 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadRequest; +import androidx.media3.exoplayer.offline.DownloadService; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; @@ -29,6 +33,7 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; +import com.google.gson.Gson; import com.hi.music.player.MusicApplication; import com.hi.music.player.R; import com.hi.music.player.adapter.AdapterPlayList; @@ -39,17 +44,20 @@ import com.hi.music.player.api.onPlayNextListener; import com.hi.music.player.databinding.ActivityPlayBinding; import com.hi.music.player.helper.CommonUtils; import com.hi.music.player.helper.MyValue; +import com.hi.music.player.javabean.BoxDownloadSong; import com.hi.music.player.javabean.BoxLikeSong; import com.hi.music.player.javabean.CustomerUrlInfo; import com.hi.music.player.javabean.response.ResponsePlayListInfo; import com.hi.music.player.javabean.response.ResponsePlayUrl; import com.hi.music.player.javabean.response.child.ResponseCategory; import com.hi.music.player.javabean.response.child.ResponseSingle; +import com.hi.music.player.media3.MyDownloadService; import com.hi.music.player.media3.MyMediaControllerManager; import com.hi.music.player.objectbox.ObjectBoxManager; import com.hi.music.player.ui.activity.viewmodel.VMApplication; import com.hi.music.player.ui.activity.viewmodel.VMPlay; +import java.nio.charset.StandardCharsets; import java.util.List; public class PlayActivity extends BaseActivity implements SeekBar.OnSeekBarChangeListener { @@ -114,7 +122,7 @@ public class PlayActivity extends BaseActivity implements S mEnterType = intent.getIntExtra(MyValue.KEY_ENTER_SOURCE, MyValue.TYPE_ENTER_SOURCE_SINGLE); initPlayerView(); initProgressHandler(); - + addDownloadListener(); CommonUtils.LogMsg("--------mEnterType=" + mEnterType); switch (mEnterType) { @@ -347,6 +355,7 @@ public class PlayActivity extends BaseActivity implements S vb.btnLoop.setOnClickListener(this); vb.layoutLike.setOnClickListener(this); vb.layoutDownload.setOnClickListener(this); + } @@ -597,11 +606,62 @@ public class PlayActivity extends BaseActivity implements S } + }else if(v.equals(vb.layoutDownload)){ + vb.downloadPb.setVisibility(View.VISIBLE); + BoxDownloadSong curMediaItemInfo = getCurMediaItemInfo(); + if(curMediaItemInfo!= null){ + Gson gson = new Gson(); + String info = gson.toJson(curMediaItemInfo); + byte[] data = info.getBytes(StandardCharsets.UTF_8); + + String videoId1 = curMediaItemInfo.getVideoId(); + CommonUtils.LogMsg("----------------开始下载 id="+videoId1); + String contentUri= "https://rr3---sn-tt1e7nlz.googlevideo.com/videoplayback?expire=1730203742&ei=_nsgZ4KrKsCW2_gP3vHpsQk&ip=146.19.167.8&id=o-AMLbkjliQ0-OoP2NcJ1EcdlaKQ7tyJP2QHMOIxrhiBKx&source=youtube&requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&met=1730182142%2C&mh=HX&mm=31%2C29&mn=sn-tt1e7nlz%2Csn-vgqsrnzd&ms=au%2Crdu&mv=m&mvi=3&pl=24&rms=au%2Cau&gcr=us&initcwndbps=241250&svpuc=1&sabr=1&rqh=1&mt=1730181899&fvip=1&keepalive=yes&fexp=51312688%2C51326932&c=ANDROID&sparams=expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Cxpc%2Cgcr%2Csvpuc%2Csabr%2Crqh&sig=AJfQdSswRgIhAOxvUuLE3tfOLtuF9OP0zmy8tl4HQvm-6BlDjOnkhyGZAiEAjjL2jOFMqrjDgHMtwkWaQb9CNXz6lNqi1ShWWzGjtjY%3D&lsparams=met%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Crms%2Cinitcwndbps&lsig=ACJ0pHgwRQIhAJ7XPfdAxtoFrNQ13TkFhqHiGYGaKaBfxC6gb9fbB369AiBfx8gshE7jEgx0wOHce2IVHrZ_4aQvYptrSh23tfZqqA%3D%3D"; + DownloadRequest downloadRequest = new DownloadRequest.Builder(videoId1, Uri.parse(contentUri)) + .setMimeType("video/mp4") + .setData(data) + .build(); + + // 启动 DownloadService 进行下载 + DownloadService.sendAddDownload( + this, + MyDownloadService.class, // 上面定义的下载服务类 + downloadRequest, + true // 是否在前台运行 + ); + } + + + + } } + @OptIn(markerClass = UnstableApi.class) + private BoxDownloadSong getCurMediaItemInfo(){ + MediaItem curMediaItem = mediaControllerManager.getCurMediaItem(); + if(curMediaItem== null){ + return null; + } + MediaMetadata mediaMetadata = curMediaItem.mediaMetadata; + if(mediaMetadata.title== null||mediaMetadata.artist== null||mediaMetadata.description==null + ||mediaMetadata.durationMs==null){ + return null; + } + + BoxDownloadSong boxDownloadSong = new BoxDownloadSong(); + boxDownloadSong.setVideoId(curMediaItem.mediaId); + boxDownloadSong.setCovert(String.valueOf(mediaMetadata.artworkUri)); + boxDownloadSong.setSongName((String) mediaMetadata.title); + boxDownloadSong.setSingerName((String) mediaMetadata.artist); + boxDownloadSong.setDuration((String) mediaMetadata.description); + boxDownloadSong.setDurationMs( mediaMetadata.durationMs); + + return boxDownloadSong; + } + /** * 控制播放列表的显示 * @@ -721,4 +781,9 @@ public class PlayActivity extends BaseActivity implements S public void onStopTrackingTouch(SeekBar seekBar) { } + + @OptIn(markerClass = UnstableApi.class) + private void addDownloadListener(){ + MyDownloadService.addDownloadListener(vmApplication); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMApplication.java b/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMApplication.java index d4ab73b..ee17cf5 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMApplication.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMApplication.java @@ -1,11 +1,19 @@ package com.hi.music.player.ui.activity.viewmodel; +import android.util.Pair; + +import androidx.annotation.OptIn; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadCursor; +import com.google.gson.Gson; import com.hi.music.player.api.RequestListener; import com.hi.music.player.helper.CommonUtils; +import com.hi.music.player.javabean.BoxDownloadSong; import com.hi.music.player.javabean.CustomerUrlInfo; import com.hi.music.player.javabean.response.ResponsePlayListInfo; import com.hi.music.player.javabean.response.ResponsePlayUrl; @@ -16,6 +24,7 @@ import com.hi.music.player.network.RetrofitManager; import org.json.JSONObject; +import java.nio.charset.StandardCharsets; import java.util.List; import okhttp3.ResponseBody; @@ -31,6 +40,10 @@ public class VMApplication extends ViewModel { public LiveData playStatus = _playStatus; + private MutableLiveData> _downloadData = new MutableLiveData<>(); + public LiveData> downloadData = _downloadData; + + /** * 重置播放列表 * @param list @@ -44,4 +57,17 @@ public class VMApplication extends ViewModel { } + @OptIn(markerClass = UnstableApi.class) + public void setDownloadData( List downloadList) { +// +// byte[] data = download.request.data; +// String additionalData = new String(data, StandardCharsets.UTF_8); +// Gson gson = new Gson(); +// BoxDownloadSong boxDownloadSong = gson.fromJson(additionalData, BoxDownloadSong.class); +// String covert = boxDownloadSong.getCovert(); +// String count = String.valueOf(downloadCount); +// +// Pair stringStringPair = new Pair(covert,count); + _downloadData.setValue(downloadList); + } } diff --git a/app/src/main/java/com/hi/music/player/ui/fragmnt/ProfileFragment.java b/app/src/main/java/com/hi/music/player/ui/fragmnt/ProfileFragment.java index 12b27f4..25c2354 100644 --- a/app/src/main/java/com/hi/music/player/ui/fragmnt/ProfileFragment.java +++ b/app/src/main/java/com/hi/music/player/ui/fragmnt/ProfileFragment.java @@ -1,56 +1,58 @@ package com.hi.music.player.ui.fragmnt; -import static com.hi.music.player.javabean.BoxLikeSong_.covert; - import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; +import android.util.Pair; import android.view.View; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.lifecycle.Observer; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.Download; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.Target; -import com.hi.music.player.MusicApplication; import com.hi.music.player.R; import com.hi.music.player.api.LikeSongListener; import com.hi.music.player.databinding.FragmentProfileBinding; import com.hi.music.player.helper.CommonUtils; +import com.hi.music.player.helper.MyValue; +import com.hi.music.player.javabean.BoxDownloadSong; import com.hi.music.player.javabean.BoxLikeSong; +import com.hi.music.player.media3.MyDownloadService; import com.hi.music.player.objectbox.ObjectBoxManager; import com.hi.music.player.ui.activity.LikeSongActivity; +import com.hi.music.player.ui.activity.viewmodel.VMApplication; import java.util.List; import io.objectbox.reactive.DataSubscription; -public class ProfileFragment extends BaseFragment implements View.OnClickListener{ +public class ProfileFragment extends BaseFragment implements View.OnClickListener { private DataSubscription dataSubscription; - private int likeSongSCount = 0; + private int likeSongCount = 0; + private int downloadSongCount = 0; + + private VMApplication vmApplication; + @Override protected FragmentProfileBinding getFragmentVb() { return FragmentProfileBinding.inflate(getLayoutInflater()); } - - + @OptIn(markerClass = UnstableApi.class) @Override protected void initView() { - dataSubscription = ObjectBoxManager.setLikeDataListener(new LikeSongListener() { + vmApplication = getApplicationScopeViewModel(VMApplication.class); + dataSubscription = ObjectBoxManager.setLikeDataListener(new LikeSongListener() { @Override public void onLikeSongChange(List data) { CommonUtils.LogMsg("------onLikeSongChange data=" + data.size()); int size = data.size(); - likeSongSCount = size; + likeSongCount = size; Vb.tvLikeSize.setText(String.format(getString(R.string.like_song), size)); if (size == 0) { Vb.likeCovert.setVisibility(View.GONE); @@ -68,8 +70,34 @@ public class ProfileFragment extends BaseFragment implem } } }); + MyDownloadService.updateDownloadUi(vmApplication); - Vb.relayoutLike.setOnClickListener(this); + vmApplication.downloadData.observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List downloads) { + int size = downloads.size(); + Vb.tvDownloadSize.setText(String.format(getString(R.string.download_song), size)); + downloadSongCount = size; + if (size > 0) { + Download download = downloads.get(downloads.size() - 1); + BoxDownloadSong boxDownloadSong = CommonUtils.downloadToBean(download); + + Vb.downloadCovert.setVisibility(View.VISIBLE); + Vb.downloadDefault.setVisibility(View.GONE); + Glide.with(requireContext()) + .asDrawable() + .load(boxDownloadSong.getCovert()) + .placeholder(R.drawable.placeholder) + .error(R.drawable.placeholder) + .into(Vb.downloadCovert); + } else { + Vb.downloadCovert.setVisibility(View.GONE); + Vb.downloadDefault.setVisibility(View.VISIBLE); + } + } + }); + + Vb.relayoutLike.setOnClickListener(this); Vb.relayoutDownload.setOnClickListener(this); } @@ -83,7 +111,7 @@ public class ProfileFragment extends BaseFragment implem @Override public void onDestroyView() { super.onDestroyView(); - if(dataSubscription!= null){ + if (dataSubscription != null) { dataSubscription.cancel(); } CommonUtils.LogMsg("------ProfileFragment-onDestroyView"); @@ -97,9 +125,16 @@ public class ProfileFragment extends BaseFragment implem @Override public void onClick(View v) { - if(v.equals(Vb.relayoutLike)){ - if(likeSongSCount>0){ + if (v.equals(Vb.relayoutLike)) { + if (likeSongCount > 0) { Intent intent = new Intent(requireActivity(), LikeSongActivity.class); + intent.putExtra(MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE, MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE_LIKE); + startActivity(intent); + } + } else if (v.equals(Vb.relayoutDownload)) { + if (downloadSongCount > 0) { + Intent intent = new Intent(requireActivity(), LikeSongActivity.class); + intent.putExtra(MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE, MyValue.KEY_ENTER_LIKE_ACTIVITY_TYPE_DOWNLOAD); startActivity(intent); } } diff --git a/app/src/main/res/layout/activity_like_song.xml b/app/src/main/res/layout/activity_like_song.xml index 28ada3b..1ca08dd 100644 --- a/app/src/main/res/layout/activity_like_song.xml +++ b/app/src/main/res/layout/activity_like_song.xml @@ -24,6 +24,7 @@ android:text="@string/text_like_song" android:textColor="@color/white" android:textSize="17sp" + android:id="@+id/tv_title" app:layout_constraintBottom_toBottomOf="@id/im_back" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" diff --git a/app/src/main/res/layout/activity_play.xml b/app/src/main/res/layout/activity_play.xml index 43cf9bc..d897204 100644 --- a/app/src/main/res/layout/activity_play.xml +++ b/app/src/main/res/layout/activity_play.xml @@ -209,7 +209,7 @@ android:indeterminateTint="@color/panel_bg" android:progressBackgroundTint="@color/panel_bg" android:progressTint="@color/panel_bg" - android:visibility="visible" /> + android:visibility="gone" /> Songs %d Offline %d Favorite Songs + Offline Songs \ No newline at end of file