From 7214b3db66a914947080eeda1e649d6d8e895c59 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 11 Apr 2022 10:29:47 +0800 Subject: [PATCH 01/19] [Fix] media notification PendingIntent mutability flag missing --- app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index fe1207cb5..d6683c71c 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -1,6 +1,7 @@ package com.zionhuang.music.playback import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -280,7 +281,7 @@ class SongPlayer( } override fun createCurrentContentIntent(player: Player): PendingIntent? = - PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0) + PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), FLAG_IMMUTABLE) }) .setChannelNameResourceId(R.string.channel_name_playback) .setNotificationListener(notificationListener) From f81e8e42cc601abd5fcb1c397004290b68d073a9 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 11 Apr 2022 10:35:15 +0800 Subject: [PATCH 02/19] [Fix] create media and artwork folder if not exist --- .../main/java/com/zionhuang/music/repos/SongRepository.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index fabbf94cc..c021cf2de 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -103,11 +103,15 @@ object SongRepository : LocalRepository { } override fun getSongFile(songId: String): File { - return context.getExternalFilesDir(null)!! / "media" / md5(songId) + val mediaDir = context.getExternalFilesDir(null)!! / "media" + if (!mediaDir.isDirectory) mediaDir.mkdirs() + return mediaDir / md5(songId) } override fun getSongArtworkFile(songId: String): File { - return context.getExternalFilesDir(null)!! / "artwork" / md5(songId) + val artworkDir = context.getExternalFilesDir(null)!! / "artwork" + if (!artworkDir.isDirectory) artworkDir.mkdirs() + return artworkDir / md5(songId) } From 98cd92d7482db5cc5bc9495aa5ac5618f639595c Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 11 Apr 2022 11:19:54 +0800 Subject: [PATCH 03/19] [Add] Share button function --- .../music/ui/fragments/BottomControlsFragment.kt | 9 +++++++++ .../com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt | 2 ++ app/src/main/res/layout/bottom_controls_sheet.xml | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 21709f216..cec6e0145 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -1,5 +1,6 @@ package com.zionhuang.music.ui.fragments +import android.content.Intent import android.os.Bundle import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.PlaybackStateCompat.STATE_NONE @@ -8,6 +9,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.constraintlayout.motion.widget.MotionLayout +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -23,6 +25,7 @@ import com.zionhuang.music.ui.widgets.BottomSheetListener import com.zionhuang.music.ui.widgets.MediaWidgetsController import com.zionhuang.music.ui.widgets.PlayPauseBehavior import com.zionhuang.music.viewmodels.PlaybackViewModel +import com.zionhuang.music.youtube.NewPipeYouTubeHelper.videoIdToUrl class BottomControlsFragment : Fragment(), BottomSheetListener, MotionLayout.TransitionListener { companion object { @@ -85,6 +88,12 @@ class BottomControlsFragment : Fragment(), BottomSheetListener, MotionLayout.Tra viewModel.transportControls?.sendCustomAction(ACTION_TOGGLE_LIKE, null) } + binding.btnShare.setOnClickListener { + viewModel.mediaData.value?.id?.let { id -> + startActivity(Intent(Intent.ACTION_VIEW, videoIdToUrl(id)?.toUri())) + } + } + with(PlayPauseBehavior(requireContext())) { binding.btnBtmPlayPause.setBehavior(this) binding.btnPlayPause.setBehavior(this) diff --git a/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt b/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt index 6f2ce09bb..0c60cbc14 100644 --- a/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt +++ b/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt @@ -26,6 +26,8 @@ object NewPipeYouTubeHelper { */ fun extractVideoId(url: String): String? = tryOrNull { service.streamLHFactory.getId(url) } + fun videoIdToUrl(id: String): String? = tryOrNull { service.streamLHFactory.getUrl(id) } + /** * Search */ diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index b91cbfef1..2c91b2ef8 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -75,6 +75,7 @@ android:layout_margin="13dp" android:padding="2dp" android:src="@drawable/ic_more_vert" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -272,7 +273,8 @@ android:background="?selectableItemBackgroundBorderless" android:clickable="true" android:scaleType="center" - android:src="@drawable/ic_favorite_border" /> + android:src="@drawable/ic_favorite_border" + android:visibility="gone" /> Date: Mon, 11 Apr 2022 11:40:44 +0800 Subject: [PATCH 04/19] [Fix] YouTubePlaylistFragment missing streamPopupMenuListener --- .../ui/fragments/YouTubePlaylistFragment.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt index 3a736386f..df31853a5 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt @@ -23,6 +23,7 @@ import com.zionhuang.music.ui.adapters.InfoItemAdapter import com.zionhuang.music.ui.adapters.LoadStateAdapter import com.zionhuang.music.ui.fragments.base.BindingFragment import com.zionhuang.music.viewmodels.PlaybackViewModel +import com.zionhuang.music.viewmodels.SongsViewModel import com.zionhuang.music.viewmodels.YouTubePlaylistViewModel import com.zionhuang.music.youtube.NewPipeYouTubeHelper import kotlinx.coroutines.flow.collectLatest @@ -36,6 +37,7 @@ class YouTubePlaylistFragment : BindingFragment() { private val playlistId by lazy { args.playlistId } private val viewModel by viewModels() + private val songsViewModel by activityViewModels() private val playbackViewModel by activityViewModels() private val infoItemAdapter = InfoItemAdapter() @@ -51,12 +53,15 @@ class YouTubePlaylistFragment : BindingFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - infoItemAdapter.addLoadStateListener { loadState -> - binding.progressBar.isVisible = loadState.refresh is LoadState.Loading - binding.btnRetry.isVisible = loadState.refresh is LoadState.Error - binding.errorMsg.isVisible = loadState.refresh is LoadState.Error - if (loadState.refresh is LoadState.Error) { - binding.errorMsg.text = (loadState.refresh as LoadState.Error).error.localizedMessage + infoItemAdapter.apply { + streamMenuListener = songsViewModel.streamPopupMenuListener + addLoadStateListener { loadState -> + binding.progressBar.isVisible = loadState.refresh is LoadState.Loading + binding.btnRetry.isVisible = loadState.refresh is LoadState.Error + binding.errorMsg.isVisible = loadState.refresh is LoadState.Error + if (loadState.refresh is LoadState.Error) { + binding.errorMsg.text = (loadState.refresh as LoadState.Error).error.localizedMessage + } } } From 3d385f853b4bb8bf4e7f12ea3aa948d8c5f1ad08 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 11 Apr 2022 12:37:53 +0800 Subject: [PATCH 05/19] [Feature] Deep link for YouTube urls --- app/src/main/AndroidManifest.xml | 659 +++++++++++++++++- .../music/ui/activities/MainActivity.kt | 41 +- .../music/youtube/NewPipeYouTubeHelper.kt | 7 +- 3 files changed, 694 insertions(+), 13 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ca3eb58b..1f337da3e 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ @@ -30,9 +30,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (), NavController.OnDestinationChangedListener { override fun getViewBinding() = ActivityMainBinding.inflate(layoutInflater) @@ -56,6 +65,26 @@ class MainActivity : BindingActivity(), NavController.OnDes // } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + when (intent?.action) { + ACTION_VIEW -> { + val url = intent.data.toString() + when (getLinkType(url)) { + LinkType.STREAM -> { + val videoId = extractVideoId(url)!! + playbackViewModel.playMedia(this, videoId, bundleOf( + EXTRA_QUEUE_DATA to QueueData(QUEUE_YT_SINGLE, queueId = videoId) + )) + } + LinkType.CHANNEL -> {} + LinkType.PLAYLIST -> {} + LinkType.NONE -> {} + } + } + } + } + private fun setupUI() { setSupportActionBar(binding.toolbar) val navHostFragment = @@ -89,11 +118,7 @@ class MainActivity : BindingActivity(), NavController.OnDes } } - override fun onDestinationChanged( - controller: NavController, - destination: NavDestination, - arguments: Bundle? - ) { + override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) { actionMode?.finish() if (destination.id == R.id.playlistsFragment) { binding.fab.show() diff --git a/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt b/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt index 0c60cbc14..b48206eb0 100644 --- a/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt +++ b/app/src/main/java/com/zionhuang/music/youtube/NewPipeYouTubeHelper.kt @@ -3,11 +3,8 @@ package com.zionhuang.music.youtube import com.zionhuang.music.extensions.tryOrNull import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext -import org.schabi.newpipe.extractor.InfoItem +import org.schabi.newpipe.extractor.* import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage -import org.schabi.newpipe.extractor.NewPipe -import org.schabi.newpipe.extractor.Page -import org.schabi.newpipe.extractor.ServiceList import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.exceptions.ExtractionException import org.schabi.newpipe.extractor.playlist.PlaylistInfo @@ -21,6 +18,8 @@ import java.io.IOException object NewPipeYouTubeHelper { private val service = NewPipe.getService(ServiceList.YouTube.serviceId) as YoutubeService + fun getLinkType(url: String): StreamingService.LinkType = service.getLinkTypeByUrl(url) + /** * Stream */ From 210668fe738d21dd205c8413c925226dab6a307f Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 11 Apr 2022 12:53:04 +0800 Subject: [PATCH 06/19] [Fix] Fragment title --- .../music/ui/fragments/YouTubeChannelFragment.kt | 3 +++ .../music/ui/fragments/YouTubePlaylistFragment.kt | 3 +++ .../music/viewmodels/YouTubeChannelViewModel.kt | 3 +++ .../music/viewmodels/YouTubePlaylistViewModel.kt | 3 +++ app/src/main/res/navigation/navigation_graph.xml | 9 +++------ 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubeChannelFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubeChannelFragment.kt index 22306651f..e418fdd80 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubeChannelFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubeChannelFragment.kt @@ -19,6 +19,7 @@ import com.zionhuang.music.databinding.LayoutRecyclerviewBinding import com.zionhuang.music.extensions.addOnClickListener import com.zionhuang.music.extensions.themeColor import com.zionhuang.music.models.QueueData +import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.ui.adapters.InfoItemAdapter import com.zionhuang.music.ui.adapters.LoadStateAdapter import com.zionhuang.music.ui.fragments.base.BindingFragment @@ -84,6 +85,8 @@ class YouTubeChannelFragment : BindingFragment() { } lifecycleScope.launch { + val channel = viewModel.getChannelInfo(channelId) + (requireActivity() as MainActivity).supportActionBar?.title = channel.name viewModel.getChannel(channelId).collectLatest { infoItemAdapter.submitData(it) } diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt index df31853a5..fa9163a00 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/YouTubePlaylistFragment.kt @@ -19,6 +19,7 @@ import com.zionhuang.music.databinding.LayoutRecyclerviewBinding import com.zionhuang.music.extensions.addOnClickListener import com.zionhuang.music.extensions.themeColor import com.zionhuang.music.models.QueueData +import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.ui.adapters.InfoItemAdapter import com.zionhuang.music.ui.adapters.LoadStateAdapter import com.zionhuang.music.ui.fragments.base.BindingFragment @@ -82,6 +83,8 @@ class YouTubePlaylistFragment : BindingFragment() { } lifecycleScope.launch { + val playlist = viewModel.getPlaylistInfo(playlistId) + (requireActivity() as MainActivity).supportActionBar?.title = playlist.name viewModel.getPlaylist(playlistId).collectLatest { infoItemAdapter.submitData(it) } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeChannelViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeChannelViewModel.kt index ee2bd2c61..86fa0870e 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeChannelViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeChannelViewModel.kt @@ -7,8 +7,11 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn import com.zionhuang.music.repos.YouTubeRepository +import com.zionhuang.music.youtube.NewPipeYouTubeHelper class YouTubeChannelViewModel(application: Application) : AndroidViewModel(application) { + suspend fun getChannelInfo(channelId: String) = NewPipeYouTubeHelper.getChannel(channelId) + fun getChannel(channelId: String) = Pager(PagingConfig(pageSize = 20)) { YouTubeRepository.getChannel(channelId) }.flow.cachedIn(viewModelScope) diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubePlaylistViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubePlaylistViewModel.kt index f2ddf41ff..c9442ae42 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubePlaylistViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubePlaylistViewModel.kt @@ -7,8 +7,11 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn import com.zionhuang.music.repos.YouTubeRepository +import com.zionhuang.music.youtube.NewPipeYouTubeHelper class YouTubePlaylistViewModel(application: Application) : AndroidViewModel(application) { + suspend fun getPlaylistInfo(playlistId: String) = NewPipeYouTubeHelper.getPlaylist(playlistId) + fun getPlaylist(playlistId: String) = Pager(PagingConfig(pageSize = 20)) { YouTubeRepository.getPlaylist(playlistId) }.flow.cachedIn(viewModelScope) diff --git a/app/src/main/res/navigation/navigation_graph.xml b/app/src/main/res/navigation/navigation_graph.xml index 1ef3f948c..79543da50 100644 --- a/app/src/main/res/navigation/navigation_graph.xml +++ b/app/src/main/res/navigation/navigation_graph.xml @@ -53,8 +53,7 @@ + android:name="com.zionhuang.music.ui.fragments.search.YouTubeSearchFragment"> @@ -110,16 +109,14 @@ + android:name="com.zionhuang.music.ui.fragments.YouTubePlaylistFragment"> + android:name="com.zionhuang.music.ui.fragments.YouTubeChannelFragment"> From 27eb81c5dd085a0f8be4db80b875cb27305d5b8d Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 12 Apr 2022 12:09:12 +0800 Subject: [PATCH 07/19] Upload APK in workflow --- .github/workflows/build.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64a46cd4f..c6db97e2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: branches: - '**' pull_request: - branches: + branches: - master - dev @@ -13,15 +13,17 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Check Lint - run: sudo ./gradlew lintDebug - - name: Run tests - run: sudo ./gradlew test - - name: Build with Gradle - run: sudo ./gradlew build + + - name: Build debug APK and run jvm tests + run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint + + - name: Upload APK + uses: actions/upload-artifact@v2 + with: + name: app + path: app/build/outputs/apk/debug/*.apk \ No newline at end of file From 164a2020d09b6516cc30e54cd3a2fb3474596c36 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 12 Apr 2022 12:13:12 +0800 Subject: [PATCH 08/19] Use JDK 11 in workflow --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6db97e2c..b02290733 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,10 +14,12 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - name: set up JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + java-version: 11 + distribution: "temurin" + cache: 'gradle' - name: Build debug APK and run jvm tests run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint From 61f0b03adfbab4aac7d5b04687a060d97163a964 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 12 Apr 2022 13:00:53 +0800 Subject: [PATCH 09/19] Set foregroundServiceType of MusicService to mediaPlayback --- app/src/main/AndroidManifest.xml | 644 +------------------------------ 1 file changed, 2 insertions(+), 642 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1f337da3e..55a393367 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,647 +49,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:exported="true" + android:foregroundServiceType="mediaPlayback"> From efb2ec4aec16a850dcebe07552081269424254ee Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 12 Apr 2022 13:22:14 +0800 Subject: [PATCH 10/19] [Feature] Search for YouTube Music albums --- .../ui/viewholders/SearchHeaderViewHolder.kt | 6 ++++-- .../viewholders/SearchPlaylistViewHolder.kt | 6 +++++- .../main/res/layout/item_search_header.xml | 19 +++++++++++++------ app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchHeaderViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchHeaderViewHolder.kt index a4e85f5af..e34117977 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchHeaderViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchHeaderViewHolder.kt @@ -18,6 +18,7 @@ class SearchHeaderViewHolder( R.id.chip_all -> ALL R.id.chip_songs -> MUSIC_SONGS R.id.chip_videos -> MUSIC_VIDEOS + R.id.chip_albums -> MUSIC_ALBUMS R.id.chip_playlists -> PLAYLISTS R.id.chip_channels -> CHANNELS else -> throw IllegalArgumentException("Unexpected filter type.") @@ -28,9 +29,10 @@ class SearchHeaderViewHolder( fun bind() { when (listener.filter) { - //ALL -> binding.chipAll + ALL -> binding.chipAll MUSIC_SONGS -> binding.chipSongs - VIDEOS -> binding.chipVideos + MUSIC_VIDEOS -> binding.chipVideos + MUSIC_ALBUMS -> binding.chipAlbums PLAYLISTS -> binding.chipPlaylists CHANNELS -> binding.chipChannels else -> null diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchPlaylistViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchPlaylistViewHolder.kt index fdf40e24e..6a0dcae5e 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchPlaylistViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/SearchPlaylistViewHolder.kt @@ -13,7 +13,11 @@ class SearchPlaylistViewHolder(private val binding: ItemSearchPlaylistBinding) : binding.root.transitionName = binding.context.resources.getString(R.string.youtube_playlist_item_transition_name, item.url) binding.playlistTitle.text = item.name binding.uploader.text = item.uploaderName - binding.streams.text = item.streamCount.toString() + if (item.streamCount > 0) { + binding.streams.text = item.streamCount.toString() + } else { + binding.thumbnail.alpha = 1f + } binding.thumbnail.load(item.thumbnailUrl) { placeholder(R.drawable.ic_music_note) roundCorner(binding.thumbnail.context.resources.getDimensionPixelSize(R.dimen.song_cover_radius)) diff --git a/app/src/main/res/layout/item_search_header.xml b/app/src/main/res/layout/item_search_header.xml index d5623136d..7096a24be 100644 --- a/app/src/main/res/layout/item_search_header.xml +++ b/app/src/main/res/layout/item_search_header.xml @@ -2,17 +2,17 @@ - + android:scrollbars="none"> @@ -38,6 +38,13 @@ android:layout_height="wrap_content" android:text="@string/search_filter_videos" /> + + - + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 56bc08c43..b13d8941a 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -98,6 +98,7 @@ 全部 歌曲 影片 + 專輯 播放清單 頻道 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 055b519c3..9c48efaaa 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,6 +122,7 @@ All Songs Videos + Albums Playlists Channels From 370ece9a90ed31a63219798ef8fe9654c5ada569 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 12 Apr 2022 13:39:21 +0800 Subject: [PATCH 11/19] [Fix] Deep link not being received --- .../music/ui/activities/MainActivity.kt | 48 +++++++++++-------- .../music/viewmodels/PlaybackViewModel.kt | 6 ++- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt index 7da7b545f..bb9b4551c 100644 --- a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt @@ -11,6 +11,7 @@ import android.view.View import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.map import androidx.navigation.NavController import androidx.navigation.NavDestination @@ -34,6 +35,8 @@ import com.zionhuang.music.viewmodels.PlaybackViewModel import com.zionhuang.music.viewmodels.SongsViewModel import com.zionhuang.music.youtube.NewPipeYouTubeHelper.extractVideoId import com.zionhuang.music.youtube.NewPipeYouTubeHelper.getLinkType +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.schabi.newpipe.extractor.StreamingService.LinkType class MainActivity : BindingActivity(), NavController.OnDestinationChangedListener { @@ -42,8 +45,8 @@ class MainActivity : BindingActivity(), NavController.OnDes private var bottomSheetCallback: BottomSheetListener? = null private lateinit var bottomSheetBehavior: BottomSheetBehavior<*> - private val songsViewModel by lazy { ViewModelProvider(this).get(SongsViewModel::class.java) } - private val playbackViewModel by lazy { ViewModelProvider(this).get(PlaybackViewModel::class.java) } + private val songsViewModel by lazy { ViewModelProvider(this)[SongsViewModel::class.java] } + private val playbackViewModel by lazy { ViewModelProvider(this)[PlaybackViewModel::class.java] } val fab: MainFloatingActionButton get() = binding.fab @@ -52,6 +55,7 @@ class MainActivity : BindingActivity(), NavController.OnDes override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupUI() + handleIntent(intent) // TODO // songsViewModel.deletedSongs.observe(this) { songs -> // Snackbar.make(binding.root, resources.getQuantityString(R.plurals.snack_bar_delete_song, songs.size, songs.size), Snackbar.LENGTH_LONG) @@ -67,15 +71,23 @@ class MainActivity : BindingActivity(), NavController.OnDes override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - when (intent?.action) { + intent?.let { handleIntent(it) } + } + + private fun handleIntent(intent: Intent) { + Log.d(TAG, "${intent.action} ${intent.data}") + when (intent.action) { ACTION_VIEW -> { val url = intent.data.toString() when (getLinkType(url)) { LinkType.STREAM -> { - val videoId = extractVideoId(url)!! - playbackViewModel.playMedia(this, videoId, bundleOf( - EXTRA_QUEUE_DATA to QueueData(QUEUE_YT_SINGLE, queueId = videoId) - )) + lifecycleScope.launch { + while (playbackViewModel.mediaSessionIsConnected.value == false) delay(100) + val videoId = extractVideoId(url)!! + playbackViewModel.playMedia(this@MainActivity, videoId, bundleOf( + EXTRA_QUEUE_DATA to QueueData(QUEUE_YT_SINGLE, queueId = videoId) + )) + } } LinkType.CHANNEL -> {} LinkType.PLAYLIST -> {} @@ -87,17 +99,14 @@ class MainActivity : BindingActivity(), NavController.OnDes private fun setupUI() { setSupportActionBar(binding.toolbar) - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navController = navHostFragment.navController - val appBarConfiguration = AppBarConfiguration( - setOf( - R.id.songsFragment, - R.id.artistsFragment, - R.id.playlistsFragment, - R.id.explorationFragment - ) - ) + val appBarConfiguration = AppBarConfiguration(setOf( + R.id.songsFragment, + R.id.artistsFragment, + R.id.playlistsFragment, + R.id.explorationFragment + )) binding.toolbar.setupWithNavController(navController, appBarConfiguration) binding.bottomNav.setupWithNavController(navController) navController.addOnDestinationChangedListener(this) @@ -129,10 +138,7 @@ class MainActivity : BindingActivity(), NavController.OnDes override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) - bottomSheetBehavior.setPeekHeight( - binding.bottomNav.height + (54 * getDensity()).toInt(), - true - ) + bottomSheetBehavior.setPeekHeight(binding.bottomNav.height + (54 * getDensity()).toInt(), true) } fun setBottomSheetListener(bottomSheetListener: BottomSheetListener) { diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt index 30b59c728..a48a882ba 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt @@ -33,7 +33,7 @@ class PlaybackViewModel(application: Application) : AndroidViewModel(application private val mediaMetadataObserver = Observer { mediaMetadata -> if (mediaMetadata != null) { _mediaData.postValue(mediaData.value?.pullMediaMetadata(mediaMetadata) - ?: mediaMetadata.toMediaData()) + ?: mediaMetadata.toMediaData()) } } @@ -54,7 +54,9 @@ class PlaybackViewModel(application: Application) : AndroidViewModel(application queueData.observeForever(queueDataObserver) } - val mediaController: LiveData = mediaSessionConnection.isConnected.map { isConnected -> + val mediaSessionIsConnected = mediaSessionConnection.isConnected + + val mediaController: LiveData = mediaSessionIsConnected.map { isConnected -> if (isConnected) mediaSessionConnection.mediaController else null } From 7596a2cac5a248d25fd54fd4e4ecc21bec477662 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 12 Apr 2022 23:50:24 +0800 Subject: [PATCH 12/19] [Improve] Handle more urls and handle share intent --- app/src/main/AndroidManifest.xml | 155 +++++++++++++++++- .../music/ui/activities/MainActivity.kt | 32 ++-- 2 files changed, 161 insertions(+), 26 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55a393367..9c891a397 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,20 +30,157 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (), NavController.OnDes } private fun handleIntent(intent: Intent) { - Log.d(TAG, "${intent.action} ${intent.data}") - when (intent.action) { - ACTION_VIEW -> { - val url = intent.data.toString() - when (getLinkType(url)) { - LinkType.STREAM -> { - lifecycleScope.launch { - while (playbackViewModel.mediaSessionIsConnected.value == false) delay(100) - val videoId = extractVideoId(url)!! - playbackViewModel.playMedia(this@MainActivity, videoId, bundleOf( - EXTRA_QUEUE_DATA to QueueData(QUEUE_YT_SINGLE, queueId = videoId) - )) - } - } - LinkType.CHANNEL -> {} - LinkType.PLAYLIST -> {} - LinkType.NONE -> {} + // Handle url + val url = (intent.data ?: intent.getStringExtra(EXTRA_TEXT)).toString() + Log.d(TAG, "${intent.action} ${url}") + when (getLinkType(url)) { + LinkType.STREAM -> { + lifecycleScope.launch { + while (playbackViewModel.mediaSessionIsConnected.value == false) delay(100) + val videoId = extractVideoId(url)!! + playbackViewModel.playMedia(this@MainActivity, videoId, bundleOf( + EXTRA_QUEUE_DATA to QueueData(QUEUE_YT_SINGLE, queueId = videoId) + )) } } + LinkType.CHANNEL -> {} + LinkType.PLAYLIST -> {} + LinkType.NONE -> {} } } From 217e512fceb84a4306d7a4146466b437debb27b8 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 13 Apr 2022 18:40:12 +0800 Subject: [PATCH 13/19] [Feature] Remove download of a song --- .../music/download/DownloadBroadcastReceiver.kt | 2 +- .../com/zionhuang/music/repos/SongRepository.kt | 9 +++++---- .../zionhuang/music/repos/base/LocalRepository.kt | 5 +++-- .../zionhuang/music/ui/fragments/SongsFragment.kt | 1 + .../music/ui/listeners/SongPopupMenuListener.kt | 4 +++- .../music/ui/viewholders/SongViewHolder.kt | 5 ++++- .../zionhuang/music/viewmodels/SongsViewModel.kt | 6 ++++++ app/src/main/res/drawable/ic_file_download.xml | 13 +++++++------ app/src/main/res/menu/song.xml | 3 +++ .../main/res/menu/song_contextual_action_bar.xml | 4 ++++ app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 12 files changed, 39 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt b/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt index 07ae1bb6b..7d892a734 100644 --- a/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt +++ b/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt @@ -29,7 +29,7 @@ class DownloadBroadcastReceiver : BroadcastReceiver() { val songId = songRepository.getDownloadEntity(id)?.songId ?: return@launch val song = songRepository.getSongById(songId) ?: return@launch songRepository.updateSong(song.copy(downloadState = if (isSuccess) STATE_DOWNLOADED else STATE_NOT_DOWNLOADED)) - songRepository.removeDownload(id) + songRepository.removeDownloadEntity(id) } } } diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index c021cf2de..93e392756 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -76,7 +76,7 @@ object SongRepository : LocalRepository { override suspend fun downloadSongs(songIds: List) = songIds.forEach { id -> // the given songs should be already added to the local repository val song = getSongById(id) ?: return@forEach - if (song.downloadState == STATE_DOWNLOADED) return@forEach + if (song.downloadState != STATE_NOT_DOWNLOADED) return@forEach updateSong(song.copy(downloadState = STATE_PREPARING)) try { val streamInfo = remoteRepository.getStream(id) @@ -95,8 +95,9 @@ object SongRepository : LocalRepository { } } - override suspend fun deleteLocalMedia(songId: String) { - val song = getSongById(songId) ?: return + override suspend fun removeDownloads(songIds: List) = songIds.forEach { songId -> + val song = getSongById(songId) ?: return@forEach + if (song.downloadState != STATE_DOWNLOADED) return@forEach if (getSongFile(songId).delete()) { updateSong(song.copy(downloadState = STATE_NOT_DOWNLOADED)) } @@ -192,7 +193,7 @@ object SongRepository : LocalRepository { override suspend fun getDownloadEntity(downloadId: Long): DownloadEntity? = withContext(IO) { downloadDao.getDownloadEntity(downloadId) } override suspend fun addDownload(item: DownloadEntity) = withContext(IO) { downloadDao.insert(item) } - override suspend fun removeDownload(downloadId: Long) = withContext(IO) { downloadDao.delete(downloadId) } + override suspend fun removeDownloadEntity(downloadId: Long) = withContext(IO) { downloadDao.delete(downloadId) } private suspend fun Song.toSongEntity() = SongEntity( diff --git a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt index 04b746490..92778f462 100644 --- a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt @@ -18,7 +18,8 @@ interface LocalRepository { suspend fun setLiked(liked: Boolean, songs: List) suspend fun downloadSongs(songIds: List) suspend fun downloadSong(songId: String) = downloadSongs(listOf(songId)) - suspend fun deleteLocalMedia(songId: String) + suspend fun removeDownloads(songIds: List) + suspend fun removeDownload(songId: String) = removeDownloads(listOf(songId)) fun getSongFile(songId: String): File fun getSongArtworkFile(songId: String): File @@ -49,5 +50,5 @@ interface LocalRepository { fun getAllDownloads(): ListWrapper suspend fun getDownloadEntity(downloadId: Long): DownloadEntity? suspend fun addDownload(item: DownloadEntity) - suspend fun removeDownload(downloadId: Long) + suspend fun removeDownloadEntity(downloadId: Long) } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/SongsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/SongsFragment.kt index d4e4a5014..f305e46c0 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/SongsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/SongsFragment.kt @@ -99,6 +99,7 @@ class SongsFragment : BindingFragment() { R.id.action_add_to_queue -> songsViewModel.songPopupMenuListener.addToQueue(songs, requireContext()) R.id.action_add_to_playlist -> songsViewModel.songPopupMenuListener.addToPlaylist(songs, requireContext()) R.id.action_download -> songsViewModel.songPopupMenuListener.downloadSongs(tracker.selection.toList(), requireContext()) + R.id.action_remove_download -> songsViewModel.songPopupMenuListener.removeDownloads(tracker.selection.toList(), requireContext()) R.id.action_delete -> songsViewModel.songPopupMenuListener.deleteSongs(songs) } true diff --git a/app/src/main/java/com/zionhuang/music/ui/listeners/SongPopupMenuListener.kt b/app/src/main/java/com/zionhuang/music/ui/listeners/SongPopupMenuListener.kt index 84eb0f4af..ddc3afebd 100644 --- a/app/src/main/java/com/zionhuang/music/ui/listeners/SongPopupMenuListener.kt +++ b/app/src/main/java/com/zionhuang/music/ui/listeners/SongPopupMenuListener.kt @@ -9,11 +9,13 @@ interface SongPopupMenuListener { fun addToQueue(songs: List, context: Context) fun addToPlaylist(songs: List, context: Context) fun downloadSongs(songIds: List, context: Context) + fun removeDownloads(songIds: List, context: Context) fun deleteSongs(songs: List) fun playNext(song: Song, context: Context) = playNext(listOf(song), context) fun addToQueue(song: Song, context: Context) = addToQueue(listOf(song), context) fun addToPlaylist(song: Song, context: Context) = addToPlaylist(listOf(song), context) - fun downloadSongs(songId: String, context: Context) = downloadSongs(listOf(songId), context) + fun downloadSong(songId: String, context: Context) = downloadSongs(listOf(songId), context) + fun removeDownload(songId: String, context: Context) = removeDownloads(listOf(songId), context) fun deleteSongs(song: Song) = deleteSongs(listOf(song)) } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/SongViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/SongViewHolder.kt index fdb90a628..03c027870 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/SongViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/SongViewHolder.kt @@ -4,6 +4,7 @@ import android.widget.PopupMenu import androidx.recyclerview.selection.ItemDetailsLookup import androidx.recyclerview.widget.RecyclerView import com.zionhuang.music.R +import com.zionhuang.music.constants.MediaConstants.STATE_DOWNLOADED import com.zionhuang.music.constants.MediaConstants.STATE_NOT_DOWNLOADED import com.zionhuang.music.databinding.ItemSongBinding import com.zionhuang.music.db.entities.Song @@ -32,12 +33,14 @@ open class SongViewHolder( R.id.action_play_next -> popupMenuListener?.playNext(song, binding.context) R.id.action_add_to_queue -> popupMenuListener?.addToQueue(song, binding.context) R.id.action_add_to_playlist -> popupMenuListener?.addToPlaylist(song, binding.context) - R.id.action_download -> popupMenuListener?.downloadSongs(song.id, binding.context) + R.id.action_download -> popupMenuListener?.downloadSong(song.id, binding.context) + R.id.action_remove_download -> popupMenuListener?.removeDownload(song.id, binding.context) R.id.action_delete -> popupMenuListener?.deleteSongs(song) } true } menu.findItem(R.id.action_download).isVisible = song.downloadState == STATE_NOT_DOWNLOADED + menu.findItem(R.id.action_remove_download).isVisible = song.downloadState == STATE_DOWNLOADED show() } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt index e1ca65d62..d09d2e537 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt @@ -126,6 +126,12 @@ class SongsViewModel(application: Application) : AndroidViewModel(application) { } } + override fun removeDownloads(songIds: List, context: Context) { + viewModelScope.launch { + songRepository.removeDownloads(songIds) + } + } + override fun deleteSongs(songs: List) { viewModelScope.launch { songRepository.deleteSongs(songs) diff --git a/app/src/main/res/drawable/ic_file_download.xml b/app/src/main/res/drawable/ic_file_download.xml index 492b41d34..56b2e31c1 100644 --- a/app/src/main/res/drawable/ic_file_download.xml +++ b/app/src/main/res/drawable/ic_file_download.xml @@ -1,9 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="@android:color/white" + android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" /> diff --git a/app/src/main/res/menu/song.xml b/app/src/main/res/menu/song.xml index 300a6a94d..68c13af2c 100644 --- a/app/src/main/res/menu/song.xml +++ b/app/src/main/res/menu/song.xml @@ -15,6 +15,9 @@ + diff --git a/app/src/main/res/menu/song_contextual_action_bar.xml b/app/src/main/res/menu/song_contextual_action_bar.xml index fbc34b622..323913877 100644 --- a/app/src/main/res/menu/song_contextual_action_bar.xml +++ b/app/src/main/res/menu/song_contextual_action_bar.xml @@ -16,6 +16,10 @@ android:id="@+id/action_download" android:icon="@drawable/ic_file_download" android:title="@string/menu_song_download" /> + 加入待播清單 加入播放清單 下載 + 刪除下載 移除 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c48efaaa..7a35903a1 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ Add to queue Add to playlist Download + Remove download Delete From 45e5cf866e375c3dd901a9d9be0d8d597b52d979 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 13 Apr 2022 19:10:59 +0800 Subject: [PATCH 14/19] [Fix] Auto download and auto add song to library --- .../zionhuang/music/playback/SongPlayer.kt | 51 ++++++++++++------- .../zionhuang/music/repos/SongRepository.kt | 7 +++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index d6683c71c..f3a5cf88c 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -13,6 +13,7 @@ import android.os.ResultReceiver import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat.* +import android.util.Log import android.util.Pair import androidx.core.content.getSystemService import androidx.core.net.toUri @@ -22,8 +23,7 @@ import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -import com.google.android.exoplayer2.Player.STATE_IDLE +import com.google.android.exoplayer2.Player.* import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor.* @@ -125,7 +125,6 @@ class SongPlayer( setAudioAttributes(audioAttributes, true) } - private var autoDownload by context.preference(R.string.pref_auto_download, false) private var autoAddSong by context.preference(R.string.pref_auto_add_song, true) private fun updateMediaData(mediaId: String, applier: MediaData.() -> Unit) { @@ -205,22 +204,8 @@ class SongPlayer( } } setCustomActionProviders(context.createCustomAction(ACTION_ADD_TO_LIBRARY, R.string.custom_action_add_to_library, R.drawable.ic_library_add) { _, _, _ -> - scope.launch { - player.currentMetadata?.let { - it.artwork - localRepository.addSong(Song( - id = it.id, - title = it.title, - artistName = it.artist, - duration = if (player.duration != C.TIME_UNSET) (player.duration / 1000).toInt() else -1, - artworkType = it.artworkType - )) - if (autoDownload) { - player.currentMetadata?.let { metadata -> - localRepository.downloadSong(metadata.id) - } - } - } + player.currentMetadata?.let { + addToLibrary(it) } }) setQueueNavigator { player, windowIndex -> player.getMediaItemAt(windowIndex).metadata.toMediaDescription() } @@ -306,6 +291,34 @@ class SongPlayer( } } + override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, @Player.DiscontinuityReason reason: Int) { + if (reason == DISCONTINUITY_REASON_AUTO_TRANSITION && autoAddSong) { + oldPosition.mediaItem?.metadata?.let { + addToLibrary(it) + } + } + } + + override fun onPlaybackStateChanged(@Player.State playbackState: Int) { + if (playbackState == STATE_ENDED && autoAddSong) { + player.currentMetadata?.let { + addToLibrary(it) + } + } + } + + private fun addToLibrary(mediaData: MediaData) { + scope.launch { + localRepository.addSong(Song( + id = mediaData.id, + title = mediaData.title, + artistName = mediaData.artist, + duration = if (player.duration != C.TIME_UNSET) (player.duration / 1000).toInt() else -1, + artworkType = mediaData.artworkType + )) + } + } + fun release() { mediaSession.apply { isActive = false diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index 93e392756..1833c1698 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -4,6 +4,7 @@ import android.app.DownloadManager import android.util.Log import androidx.core.content.getSystemService import androidx.core.net.toUri +import com.zionhuang.music.R import com.zionhuang.music.constants.MediaConstants.STATE_DOWNLOADED import com.zionhuang.music.constants.MediaConstants.STATE_DOWNLOADING import com.zionhuang.music.constants.MediaConstants.STATE_NOT_DOWNLOADED @@ -17,6 +18,7 @@ import com.zionhuang.music.db.entities.* import com.zionhuang.music.extensions.TAG import com.zionhuang.music.extensions.div import com.zionhuang.music.extensions.getApplication +import com.zionhuang.music.extensions.preference import com.zionhuang.music.models.ListWrapper import com.zionhuang.music.models.base.ISortInfo import com.zionhuang.music.repos.base.LocalRepository @@ -38,6 +40,8 @@ object SongRepository : LocalRepository { private val downloadDao: DownloadDao = musicDatabase.downloadDao private val remoteRepository: RemoteRepository = YouTubeRepository + private var autoDownload by context.preference(R.string.pref_auto_download, false) + override suspend fun getSongById(songId: String): Song? = withContext(IO) { songDao.getSong(songId) } override fun searchSongs(query: String) = ListWrapper( getPagingSource = { songDao.searchSongsAsPagingSource(query) } @@ -51,6 +55,9 @@ object SongRepository : LocalRepository { songDao.insert(listOf(it.toSongEntity().copy( duration = if (it.duration == -1) stream.duration.toInt() else it.duration) )) + if (autoDownload) { + downloadSong(it.id) + } } catch (e: Exception) { // TODO: Handle error Log.d(TAG, e.localizedMessage.orEmpty()) From 6d4c92f6f53b7d4f49f02a23c4736bcf7cf92a86 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 13 Apr 2022 19:32:01 +0800 Subject: [PATCH 15/19] Bump NewPipe Extractor from 0.21.13 to 0.21.14 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a53a221b0..e093e6c53 100755 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } ext { kotlin_version = "1.6.10" - newpipe_version = "0.21.13" + newpipe_version = "0.21.14" } dependencies { classpath "com.android.tools.build:gradle:7.1.0" From aa705f4c285700deba2214d04fcfb2501c1e0bbb Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 13 Apr 2022 19:33:06 +0800 Subject: [PATCH 16/19] Fix typo --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a35903a1..ba1301951 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,8 +22,8 @@ Add song to your library when it completes playing Expand bottom player on play About - App Version - NewPipe Extractor Version + App version + NewPipe Extractor version Check for updates Checking… You are up to date. From f86e43cca449f18c663aad91424e4fffdc9181b3 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 14 Apr 2022 14:11:05 +0800 Subject: [PATCH 17/19] Update proguard rule for Gson and Kotlin serialization --- app/proguard-rules.pro | 60 +++++++++++++++++++ .../java/com/zionhuang/music/update/Asset.kt | 15 ++--- .../com/zionhuang/music/update/Release.kt | 19 +++--- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 114540510..0bd655ee7 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -35,3 +35,63 @@ -keep class org.ocpsoft.prettytime.i18n.** { *; } -keep class org.mozilla.javascript.** { *; } + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken +##---------------End: proguard configuration for Gson ---------- + +## Kotlin Serialization +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault diff --git a/app/src/main/java/com/zionhuang/music/update/Asset.kt b/app/src/main/java/com/zionhuang/music/update/Asset.kt index 88161f82a..98c3aa658 100644 --- a/app/src/main/java/com/zionhuang/music/update/Asset.kt +++ b/app/src/main/java/com/zionhuang/music/update/Asset.kt @@ -3,13 +3,10 @@ package com.zionhuang.music.update import com.google.gson.annotations.SerializedName data class Asset( - val id: String, - @SerializedName("node_id") - val nodeId: String, - val name: String, - @SerializedName("content_type") - val contentType: String, - val size: Long, - @SerializedName("browser_download_url") - val downloadUrl: String, + @SerializedName("id") val id: String, + @SerializedName("node_id") val nodeId: String, + @SerializedName("name") val name: String, + @SerializedName("content_type") val contentType: String, + @SerializedName("size") val size: Long, + @SerializedName("browser_download_url") val downloadUrl: String, ) diff --git a/app/src/main/java/com/zionhuang/music/update/Release.kt b/app/src/main/java/com/zionhuang/music/update/Release.kt index 263b8fa18..851d1018b 100644 --- a/app/src/main/java/com/zionhuang/music/update/Release.kt +++ b/app/src/main/java/com/zionhuang/music/update/Release.kt @@ -3,17 +3,14 @@ package com.zionhuang.music.update import com.google.gson.annotations.SerializedName data class Release( - val id: Int, - val url: String, - @SerializedName("node_id") - val nodeId: String, - @SerializedName("tag_name") - val tagName: String, - val name: String, - @SerializedName("prerelease") - val preRelease: Boolean, - val body: String, - val assets: List, + @SerializedName("id") val id: Int, + @SerializedName("url") val url: String, + @SerializedName("node_id") val nodeId: String, + @SerializedName("tag_name") val tagName: String, + @SerializedName("name") val name: String, + @SerializedName("prerelease") val preRelease: Boolean, + @SerializedName("body") val body: String, + @SerializedName("assets") val assets: List, ) { val version: Version get() = Version.parse(name) } From 7238c6d78253cbd03e46e5fd8f6b4b484f10ff72 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 14 Apr 2022 18:56:43 +0800 Subject: [PATCH 18/19] 0.2.0-beta --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 60f8c84d9..a87ec2fd6 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.zionhuang.music" minSdkVersion 26 targetSdkVersion 31 - versionCode 4 - versionName "0.1.3-beta" + versionCode 5 + versionName "0.2.0-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } applicationVariants.all { variant -> From 4f0da7e363edcda072f9012cdeeea52031f1df99 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 14 Apr 2022 19:15:12 +0800 Subject: [PATCH 19/19] 0.2.0-beta Fastlane changelog --- .../metadata/android/en-US/changelogs/5.txt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/5.txt diff --git a/fastlane/metadata/android/en-US/changelogs/5.txt b/fastlane/metadata/android/en-US/changelogs/5.txt new file mode 100644 index 000000000..371bc10f9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5.txt @@ -0,0 +1,27 @@ +
New + +* App updater (Preview) +* Share button function in bottom control fragment +* Open YouTube urls by this app #11 +* You can now search for YouTube Music albums +* Remove download of a song + +
Improved + +* Show channel or playlist name in search fragment title +* Scrollable search filter chip group + +
Fixed + +* Fixed artwork updates incorrectly when preloading the next song +* Fixed play artist songs not working +* Fixed media notification PendingIntent mutability flag missing, causing Android S+ to crash #6 +* Fixed can't add songs to library #9 +* Auto download not working +* Auto add song to library not working + +
Development + +* Bumped NewPipe Extractor from 0.21.13 to 0.21.14 +* Upload APK in GitHub Action +* Added Fastlane structure \ No newline at end of file