Skip to content

Commit

Permalink
add SamplePlayer
Browse files Browse the repository at this point in the history
  • Loading branch information
mag0716 authored Jan 24, 2022
0 parents commit 029dc09
Show file tree
Hide file tree
Showing 58 changed files with 1,842 additions and 0 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# README

## Things I want to do

* The app shows currently playing items
* The app allows reordering during the shuffle
* The result of reordering during the shuffle does not affect the song order before the shuffle

## Current approach

* Get currently playing items from [MediaControllerCompat.Callback#onQueueChanged](https://developer.android.com/reference/kotlin/android/support/v4/media/session/MediaControllerCompat.Callback#onqueuechanged)
* Customize [TimelineQueueEditor](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.html), [ShuffleOrder](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/source/ShuffleOrder.html)
* doesn't call [Player#moveMediaItem](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#moveMediaItem(int,int)) when reorder during the shuffle
* generate a new `ShuffleOrder` from the sorted result
* call [ExoPlayer#setShuffleOrder](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ExoPlayer.html#setShuffleOrder(com.google.android.exoplayer2.source.ShuffleOrder)) to update the sort order

## Result

* When reordering several times while shuffling, the song order may not be reflected in MediaSession
* doesn't call [MediaControllerCompat.Callback#onQueueChanged](https://developer.android.com/reference/kotlin/android/support/v4/media/session/MediaControllerCompat.Callback#onqueuechanged)

## Reproduction Steps

1. Play music
1. Enable shuffling
1. Reorder playlist items several times
1. Wait to seek to next media item

## Environment

* ExoPlayer:2.16.1
* Test Device:
* Pixel 3(OS 12)
* Pixel 4(OS 11)
* Emulator(OS 10)

## Music

Music provided by the [Free Music Archive](http://freemusicarchive.org/).

* [Irsen's Tale](http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/) by
[Kai Engel](http://freemusicarchive.org/music/Kai_Engel/) is licensed under [CC BY-NC 3.0](https://creativecommons.org/licenses/by-nc/3.0/).
52 changes: 52 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
compileSdk 31

defaultConfig {
applicationId "com.example.android.sampleplayer"
minSdk 23
targetSdk 31
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'

implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-service:2.3.1'

implementation 'com.google.android.exoplayer:exoplayer-core:2.16.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.16.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.16.1'

implementation 'com.jakewharton.timber:timber:5.0.1'
}
21 changes: 21 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
32 changes: 32 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.sampleplayer">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SamplePlayer"
android:name=".App">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".MusicService"
android:exported="false">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>

</manifest>
11 changes: 11 additions & 0 deletions app/src/main/java/com/example/android/sampleplayer/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.android.sampleplayer

import android.app.Application
import timber.log.Timber

class App : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
}
}
151 changes: 151 additions & 0 deletions app/src/main/java/com/example/android/sampleplayer/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.example.android.sampleplayer

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.activity.viewModels
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

private val mainViewModel by viewModels<MainViewModel> {
MainViewModel.Factory(MusicServiceConnection(application))
}

private lateinit var textSongTitle: TextView
private lateinit var buttonPlayOrPause: ImageButton
private lateinit var buttonShuffle: ImageButton
private lateinit var queueList: RecyclerView

private val queueListAdapter: Adapter = Adapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

textSongTitle = findViewById(R.id.text_song_name)
buttonPlayOrPause = findViewById(R.id.button_play_or_pause)
buttonPlayOrPause.setOnClickListener {
mainViewModel.playOrPause()
}
buttonShuffle = findViewById(R.id.button_shuffle)
buttonShuffle.setOnClickListener {
mainViewModel.toggleShuffleMode()
}
queueList = findViewById(R.id.list_song_queue)
queueList.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = queueListAdapter
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
mainViewModel.move(viewHolder.absoluteAdapterPosition, target.absoluteAdapterPosition)
return true
}

override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
mainViewModel.finishMove()
}

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// not support
}

override fun isLongPressDragEnabled() = true

}).attachToRecyclerView(queueList)

mainViewModel.isReady.observe(this) {
buttonPlayOrPause.isEnabled = it
}
mainViewModel.playingMediaTitle.observe(this) {
textSongTitle.text = it
}
mainViewModel.isPlaying.observe(this) {
buttonPlayOrPause.setImageResource(
if(it) {
R.drawable.ic_baseline_pause_24
} else {
R.drawable.ic_baseline_play_arrow_24
}
)
}
mainViewModel.shuffleModeEnabled.observe(this) {
buttonShuffle.setImageResource(
if(it) {
R.drawable.ic_baseline_shuffle_on_24
} else {
R.drawable.ic_baseline_shuffle_24
}
)
}
mainViewModel.currentQueue.observe(this) {
queueListAdapter.update(it)
}

mainViewModel.initialize()
}

private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {

private val songList = mutableListOf<Song>()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_song, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = songList[position].name
}

override fun getItemCount() = songList.size

fun update(songList: List<Song>) {
val diffCallback = DiffCallback(this.songList, songList)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.songList.apply {
clear()
addAll(songList)
}
diffResult.dispatchUpdatesTo(this)
}
}

private inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.text_song_name)
}

private inner class DiffCallback(
private val oldSongList: List<Song>,
private val newSongList: List<Song>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldSongList.size

override fun getNewListSize() = newSongList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldSongList[oldItemPosition].id == newSongList[newItemPosition].id

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldSongList[oldItemPosition] == newSongList[newItemPosition]

}
}
Loading

0 comments on commit 029dc09

Please sign in to comment.