update
This commit is contained in:
parent
ac26183890
commit
058416facd
@ -58,6 +58,7 @@ dependencies {
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.media3:media3-session:1.3.1")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
@ -71,8 +72,12 @@ dependencies {
|
||||
implementation("com.github.lihangleo2:ShadowLayout:3.4.0")
|
||||
|
||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||
implementation("androidx.media3:media3-exoplayer-dash:1.3.1")
|
||||
implementation("androidx.media3:media3-ui:1.3.1")
|
||||
implementation("androidx.media3:media3-common:1.3.1")
|
||||
// implementation("com.android.tools.compose:compose-preview-renderer:0.0.1-alpha01")
|
||||
// implementation("org.chromium.net:cronet-api:119.6045.31")
|
||||
implementation("androidx.media3:media3-datasource-cronet:1.3.1")
|
||||
|
||||
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||
implementation("io.ktor:ktor-client-core:2.3.8")
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission
|
||||
@ -76,6 +77,17 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.MyDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:ignore="ForegroundServicePermission">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -9,10 +9,13 @@ import android.os.Message
|
||||
import android.view.View
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
@ -22,9 +25,17 @@ import com.player.musicoo.R
|
||||
import com.player.musicoo.adapter.PlayListAdapter
|
||||
import com.player.musicoo.databinding.ActivityMoPlayDetailsBinding
|
||||
import com.player.musicoo.innertube.Innertube
|
||||
import com.player.musicoo.innertube.models.bodies.PlayerBody
|
||||
import com.player.musicoo.innertube.requests.player
|
||||
import com.player.musicoo.media.MediaControllerManager
|
||||
import com.player.musicoo.media.SongRadio
|
||||
import com.player.musicoo.service.LoginRequiredException
|
||||
import com.player.musicoo.service.MyDownloadService
|
||||
import com.player.musicoo.service.PlayableFormatNotFoundException
|
||||
import com.player.musicoo.service.UnplayableException
|
||||
import com.player.musicoo.service.VideoIdMismatchException
|
||||
import com.player.musicoo.sp.AppStore
|
||||
import com.player.musicoo.util.DemoUtil
|
||||
import com.player.musicoo.util.LogTag.LogD
|
||||
import com.player.musicoo.util.PlayMode
|
||||
import com.player.musicoo.util.asMediaItem
|
||||
@ -32,6 +43,7 @@ import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.selects.select
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@ -299,6 +311,61 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.downloadBtn.setOnClickListener {
|
||||
if (meController != null && meController.currentMediaItem != null) {
|
||||
val contentId = meController.currentMediaItem?.mediaId!!
|
||||
|
||||
val downloadManager = DemoUtil.getDownloadManager(this)
|
||||
val downloadIndex = downloadManager.downloadIndex
|
||||
downloadIndex.getDownloads()
|
||||
.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val download = cursor.download
|
||||
if(download.request.id == contentId){
|
||||
return@setOnClickListener
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogD(TAG, "download get contentUrl")
|
||||
val urlResult = runBlocking(Dispatchers.IO) {
|
||||
Innertube.player(PlayerBody(videoId = contentId))
|
||||
}?.mapCatching { body ->
|
||||
if (body.videoDetails?.videoId != contentId) {
|
||||
throw VideoIdMismatchException()
|
||||
}
|
||||
|
||||
when (val status = body.playabilityStatus?.status) {
|
||||
"OK" -> body.streamingData?.highestQualityFormat?.let { format ->
|
||||
format.url
|
||||
} ?: throw PlayableFormatNotFoundException()
|
||||
|
||||
"UNPLAYABLE" -> throw UnplayableException()
|
||||
"LOGIN_REQUIRED" -> throw LoginRequiredException()
|
||||
else -> throw PlaybackException(
|
||||
status,
|
||||
null,
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
urlResult?.getOrThrow()?.let { url ->
|
||||
val contentUrl = url.toUri()
|
||||
LogD(TAG, "download contentUrl->${contentUrl}")
|
||||
val downloadRequest = DownloadRequest.Builder(contentId, contentUrl)
|
||||
.build()
|
||||
DownloadService.sendAddDownload(
|
||||
this,
|
||||
MyDownloadService::class.java,
|
||||
downloadRequest,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayModeUi() {
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
package com.player.musicoo.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.NotificationUtil
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.util.DemoUtil
|
||||
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class MyDownloadService : DownloadService(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||
R.string.downloads,
|
||||
0
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
private const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("ServiceCast")
|
||||
override fun getDownloadManager(): DownloadManager {
|
||||
val downloadManager = DemoUtil.getDownloadManager(this)
|
||||
val downloadNotificationHelper = DemoUtil.getDownloadNotificationHelper(this)
|
||||
downloadManager.addListener(
|
||||
TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1
|
||||
)
|
||||
)
|
||||
return downloadManager
|
||||
}
|
||||
|
||||
override fun getScheduler(): PlatformScheduler? {
|
||||
return if (Util.SDK_INT >= 21) PlatformScheduler(this, JOB_ID) else null
|
||||
}
|
||||
|
||||
override fun getForegroundNotification(
|
||||
downloads: MutableList<Download>,
|
||||
notMetRequirements: Int
|
||||
): Notification {
|
||||
return DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.buildProgressNotification(
|
||||
this,
|
||||
R.mipmap.musicoo_logo_img,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
notMetRequirements
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private class TerminalStateNotificationHelper(
|
||||
private val context: Context,
|
||||
private val notificationHelper: DownloadNotificationHelper,
|
||||
private var nextNotificationId: Int
|
||||
) :
|
||||
DownloadManager.Listener {
|
||||
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
val notification: Notification = if (download.state == Download.STATE_COMPLETED) {
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
R.mipmap.ic_download_done,
|
||||
null,
|
||||
Util.fromUtf8Bytes(download.request.data)
|
||||
)
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
context,
|
||||
R.mipmap.ic_download_done,
|
||||
null,
|
||||
Util.fromUtf8Bytes(download.request.data)
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
NotificationUtil.setNotification(context, nextNotificationId++, notification)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
182
app/src/main/java/com/player/musicoo/util/DemoUtil.java
Normal file
182
app/src/main/java/com/player/musicoo/util/DemoUtil.java
Normal file
@ -0,0 +1,182 @@
|
||||
package com.player.musicoo.util;
|
||||
import android.content.Context;
|
||||
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.database.DatabaseProvider;
|
||||
import androidx.media3.database.StandaloneDatabaseProvider;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DefaultDataSource;
|
||||
import androidx.media3.datasource.DefaultHttpDataSource;
|
||||
import androidx.media3.datasource.cache.Cache;
|
||||
import androidx.media3.datasource.cache.CacheDataSource;
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor;
|
||||
import androidx.media3.datasource.cache.SimpleCache;
|
||||
import androidx.media3.datasource.cronet.CronetDataSource;
|
||||
import androidx.media3.datasource.cronet.CronetUtil;
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.offline.DownloadManager;
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper;
|
||||
import java.io.File;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Utility methods for the demo app. */
|
||||
public final class DemoUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
/**
|
||||
* Whether the demo application uses Cronet for networking. Note that Cronet does not provide
|
||||
* automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
|
||||
*
|
||||
* <p>If set to false, the platform's default network stack is used with a {@link CookieManager}
|
||||
* configured in {@link #getHttpDataSourceFactory}.
|
||||
*/
|
||||
private static final boolean USE_CRONET_FOR_NETWORKING = true;
|
||||
|
||||
private static final String TAG = "DemoUtil";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
private static DataSource.Factory dataSourceFactory;
|
||||
private static DataSource.Factory httpDataSourceFactory;
|
||||
private static DatabaseProvider databaseProvider;
|
||||
private static File downloadDirectory;
|
||||
private static Cache downloadCache;
|
||||
private static DownloadManager downloadManager;
|
||||
// private static @MonotonicNonNull DownloadTracker downloadTracker;
|
||||
private static DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public static boolean useExtensionRenderers() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
public static RenderersFactory buildRenderersFactory(
|
||||
Context context, boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(context.getApplicationContext())
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
if (USE_CRONET_FOR_NETWORKING) {
|
||||
context = context.getApplicationContext();
|
||||
@Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
|
||||
if (cronetEngine != null) {
|
||||
httpDataSourceFactory =
|
||||
new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
}
|
||||
if (httpDataSourceFactory == null) {
|
||||
// We don't want to use Cronet, or we failed to instantiate a CronetEngine.
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
|
||||
}
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
DefaultDataSource.Factory upstreamFactory =
|
||||
new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context));
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
// public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
// ensureDownloadManagerInitialized(context);
|
||||
// return downloadTracker;
|
||||
// }
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(/* nThreads= */ 6));
|
||||
// downloadTracker =
|
||||
// new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new StandaloneDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(/* type= */ null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
||||
18
app/src/main/res/drawable/download_icon.xml
Normal file
18
app/src/main/res/drawable/download_icon.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.85"/>
|
||||
<path
|
||||
android:pathData="M12,8.5V15.5M12,15.5L14.5,13M12,15.5L9.5,13"
|
||||
android:strokeAlpha="0.85"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@ -175,6 +175,18 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/downloadBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/download_icon" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.player.musicoo.view.MarqueeTextView
|
||||
android:id="@+id/name_tv"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
BIN
app/src/main/res/mipmap-xxhdpi/ic_download_done.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_download_done.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 450 B |
@ -27,4 +27,6 @@
|
||||
<string name="history">History</string>
|
||||
<string name="no_found">No Found</string>
|
||||
<string name="more">More</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user