progmobile

This commit is contained in:
Francesco Mecca 2020-06-16 19:25:06 +02:00
parent 35a4cabdc3
commit 31cb7db080
271 changed files with 26001 additions and 0 deletions

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,38 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.apollon"
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.media:media:1.0.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation("com.squareup.okhttp3:okhttp:4.1.0")
implementation 'com.squareup.picasso:picasso:2.71828'
}

View file

@ -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

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.apollon">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<!-- configChanges prevents fragments destruction on screen orientation change -->
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".PlayerService">
</service>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -0,0 +1,128 @@
package com.apollon
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.view.View
import android.view.View.OnClickListener
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.apollon.fragments.LoginFragment
import com.apollon.fragments.PlayerFragment
class MainActivity : AppCompatActivity(), OnClickListener {
lateinit var player: PlayerService
lateinit var mediaController: MediaControllerCompat
lateinit var callback: Callback
lateinit var miniPlayer: View
lateinit var albumArt: ImageView
lateinit var playButton: Button
lateinit var title: TextView
lateinit var artist: TextView
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as PlayerService.LocalBinder
player = binder.service
mediaController = player.getSessionController()
callback = Callback()
mediaController.registerCallback(callback)
}
override fun onServiceDisconnected(name: ComponentName) {}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val intent = Intent(this, PlayerService::class.java)
startService(intent)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
/**** GUI ****/
miniPlayer = findViewById(R.id.mini_player)
albumArt = findViewById(R.id.album_art)
title = findViewById(R.id.song_title)
artist = findViewById(R.id.song_artist)
playButton = findViewById(R.id.button_play)
miniPlayer.setOnClickListener(this)
playButton.setOnClickListener(this)
findViewById<Button>(R.id.button_previous).setOnClickListener(this)
findViewById<Button>(R.id.button_next).setOnClickListener(this)
replaceFragment(LoginFragment(), false)
}
override fun onDestroy() {
unbindService(serviceConnection)
mediaController.unregisterCallback(callback)
super.onDestroy()
}
fun replaceFragment(frag: Fragment, addToStack: Boolean = true) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.main, frag)
// adds the transaction to a stack so it can be re-executed by pressing the back button
if (addToStack)
transaction.addToBackStack("ApollonStack")
transaction.commit()
supportFragmentManager.executePendingTransactions()
}
fun refreshFragment(frag: Fragment) {
val transaction = supportFragmentManager.beginTransaction()
transaction.detach(frag).attach(frag).commit()
supportFragmentManager.executePendingTransactions()
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.button_play ->
if (mediaController.playbackState.state == PlaybackStateCompat.STATE_PAUSED)
mediaController.transportControls.play()
else if (mediaController.playbackState.state == PlaybackStateCompat.STATE_PLAYING)
mediaController.transportControls.pause()
R.id.button_previous ->
mediaController.transportControls.skipToPrevious()
R.id.button_next ->
mediaController.transportControls.skipToNext()
R.id.mini_player -> {
replaceFragment(PlayerFragment())
player.echoCurrentSong()
}
}
}
inner class Callback : MediaControllerCompat.Callback() {
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
super.onPlaybackStateChanged(state)
if (state?.state == PlaybackStateCompat.STATE_PLAYING)
playButton.setBackgroundResource(R.drawable.pause_button_selector)
else if (state?.state == PlaybackStateCompat.STATE_PAUSED)
playButton.setBackgroundResource(R.drawable.play_button_selector)
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
super.onMetadataChanged(metadata)
if (metadata != null) { // No songs to play
title.text = metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)
artist.text = metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)
albumArt.setImageBitmap(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART))
}
}
}
}

View file

@ -0,0 +1,539 @@
@file:Suppress("ControlFlowWithEmptyBody", "PrivatePropertyName")
package com.apollon
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.media.*
import android.os.*
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.widget.*
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.apollon.classes.Song
import com.apollon.classes.StreamingSong
import com.squareup.picasso.Picasso
import java.io.IOException
import java.lang.Exception
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.collections.ArrayList
import kotlin.random.Random
class PlayerService : Service(), MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnInfoListener,
MediaPlayer.OnBufferingUpdateListener, AudioManager.OnAudioFocusChangeListener {
private val CHANNEL_ID = "101010"
private val PAUSE_ACTION = "PAUSE"
private val PLAY_ACTION = "PLAY"
private val PREVIOUS_ACTION = "PREVIOUS"
private val NEXT_ACTION = "NEXT"
// Binder given to clients
private val binder = LocalBinder()
private var mediaPlayer: MediaPlayer? = null
private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaController: MediaControllerCompat
private lateinit var stateBuilder: PlaybackStateCompat.Builder
private lateinit var metaDataBuilder: MediaMetadataCompat.Builder
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var audioManager: AudioManager
private lateinit var focusRequest: AudioFocusRequest
private val handler = Handler()
private var loopPlaylist = false
private var loopSong = false
private var randomSelection = false
private var playlist = ArrayList<Song>()
private var songIndex = 0
private var ready = false
private lateinit var target: com.squareup.picasso.Target
private var buffer = 0
private var source = ""
private var start = 0L
override fun onCreate() {
super.onCreate()
stateBuilder = PlaybackStateCompat.Builder()
// Media session
mediaSession = MediaSessionCompat(baseContext, "MediaSession").apply {
setPlaybackState(stateBuilder.build())
// MySessionCallback() has methods that handle callbacks from a media controller
setCallback(Callback())
}
stateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
mediaController = mediaSession.controller
metaDataBuilder = MediaMetadataCompat.Builder()
notificationManager = NotificationManagerCompat.from(this)
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
setAudioAttributes(AudioAttributes.Builder().run {
setUsage(AudioAttributes.USAGE_MEDIA)
setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
build()
})
setAcceptsDelayedFocusGain(true)
setOnAudioFocusChangeListener(FocusChangeListener(), handler)
build()
}
mediaController.registerCallback(NotificationCallback())
// Notification channel
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.player_notifications_channel), NotificationManager.IMPORTANCE_LOW)
// Register the channel with the system
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
// The system calls this method when an activity, requests the service be started
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (intent.action) {
PLAY_ACTION -> mediaSession.controller.transportControls.play()
PAUSE_ACTION -> mediaSession.controller.transportControls.pause()
NEXT_ACTION -> mediaSession.controller.transportControls.skipToNext()
PREVIOUS_ACTION -> mediaSession.controller.transportControls.skipToPrevious()
}
return super.onStartCommand(intent, flags, startId)
}
override fun onUnbind(intent: Intent?): Boolean {
stopSelf()
// allow rebind
return false
}
override fun onDestroy() {
notificationManager.cancelAll()
mediaSession.release()
mediaPlayer?.release()
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onBufferingUpdate(mp: MediaPlayer, percent: Int) {
// always returns the correct percentage w.r.t the entire duration of the song
buffer = ((start + (percent.toFloat() / 100 * mediaPlayer!!.duration)) / (start + mediaPlayer!!.duration) * 100).toInt()
val b = Bundle()
b.putInt("percent", buffer)
mediaSession.sendSessionEvent("Buffered", b)
}
override fun onCompletion(mp: MediaPlayer) {
// Invoked when playback of a media source has completed.
if (songIndex == playlist.size - 1 && !loopPlaylist && !randomSelection) {
mediaController.transportControls.stop()
} else {
nextMedia()
}
}
// Handle errors
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
// Invoked when there has been an error during an asynchronous operation
when (what) {
MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK -> Log.d("MediaPlayer Error", "MEDIA ERROR NOT VALID FOR PROGRESSIVE PLAYBACK $extra")
MediaPlayer.MEDIA_ERROR_SERVER_DIED -> Log.d("MediaPlayer Error", "MEDIA ERROR SERVER DIED $extra")
MediaPlayer.MEDIA_ERROR_UNKNOWN -> Log.d("MediaPlayer Error", "MEDIA ERROR UNKNOWN $extra")
}
return true
}
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
// Invoked to communicate some info.
return false
}
override fun onPrepared(mp: MediaPlayer) {
// Invoked when the media source is ready for playback.\
ready = true
mediaController.transportControls.play()
}
override fun onSeekComplete(mp: MediaPlayer) {
// Invoked indicating the completion of a seek operation.
}
override fun onAudioFocusChange(focusChange: Int) {
// Invoked when the audio focus of the system is updated.
}
private fun setSongURL() {
val s = SingleSong(playlist[songIndex].id)
s.execute()
while (true) {
try {
s.get(50, TimeUnit.MILLISECONDS)
break
} catch (e: TimeoutException) {
}
}
val str = s.get()
try {
val uu: String = str.url
source = uu
} catch (ex: IOException) {
Toast.makeText(applicationContext, getString(R.string.unsupported_format), Toast.LENGTH_SHORT)
.show()
}
}
fun initMedia(playlist: ArrayList<Song>, songIndex: Int) {
// initialize the media player if null
if (mediaPlayer == null)
initMediaPlayer()
// if a new song is played or the media is not ready
if (this.playlist != playlist || this.songIndex != songIndex || !ready) {
// initialise parameters
// set starting point
start = 0
ready = false
buffer = 0
this.playlist = playlist
this.songIndex = songIndex
if (mediaPlayer?.isLooping == true)
mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ALL)
// get a clean state
mediaPlayer?.reset()
// request song to server
val s = SingleSong(playlist[songIndex].id)
s.execute()
while (true) {
try {
s.get(50, TimeUnit.MILLISECONDS)
break
} catch (e: TimeoutException) {
}
}
val str = s.get()
try {
val uu: String = str.url
// set other parameters
mediaPlayer?.setDataSource(uu)
source = uu
mediaPlayer?.prepareAsync()
// MetaData builder with song information
val builder: MediaMetadataCompat.Builder = songToMetaData(str)
if (!this::target.isInitialized) {
target = object : com.squareup.picasso.Target {
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
Log.e("Picasso", e?.message)
}
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
mediaSession.setMetadata(builder.build())
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
// default bitmap
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(resources, R.drawable.default_song))
mediaSession.setMetadata(builder.build())
}
}
}
// Loads bitmap in MetaData
if (playlist[songIndex].img_url.isNotEmpty())
Picasso.get().load(playlist[songIndex].img_url).into(target)
else {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(resources, R.drawable.default_song))
mediaSession.setMetadata(builder.build())
}
} catch (ex: IOException) {
Toast.makeText(applicationContext, getString(R.string.unsupported_format), Toast.LENGTH_SHORT)
.show()
}
} else
echoCurrentSong()
}
fun initMediaFrom(pos: Long) {
start = pos
if (mediaPlayer == null)
initMediaPlayer()
ready = false
val from = (pos.toFloat() / (start + mediaPlayer!!.duration) * 100).toInt()
mediaPlayer?.reset()
mediaPlayer?.setDataSource("$source+$from")
mediaPlayer?.prepareAsync()
}
private fun initMediaPlayer() {
mediaPlayer?.release()
mediaPlayer = MediaPlayer()
// Set up MediaPlayer event listeners
mediaPlayer?.setOnCompletionListener(this)
mediaPlayer?.setOnErrorListener(this)
mediaPlayer?.setOnPreparedListener(this)
mediaPlayer?.setOnBufferingUpdateListener(this)
mediaPlayer?.setOnSeekCompleteListener(this)
mediaPlayer?.setOnInfoListener(this)
mediaPlayer?.setOnBufferingUpdateListener(this)
mediaPlayer?.setAudioAttributes(AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build())
}
private fun nextMedia() {
when {
randomSelection -> {
var ran = songIndex
while (ran == songIndex) ran = Random.nextInt(0, playlist.size - 1)
initMedia(playlist, ran)
}
loopPlaylist -> initMedia(playlist, (songIndex + 1) % playlist.size)
loopSong -> mediaPlayer?.seekTo(0)
else -> initMedia(playlist, minOf(songIndex + 1, playlist.size - 1))
}
}
fun getCurrentPosition(): Int {
return start.toInt() + (mediaPlayer?.currentPosition ?: 0)
}
fun getSessionController(): MediaControllerCompat {
return mediaSession.controller
}
fun echoCurrentSong() {
mediaSession.setMetadata(mediaController.metadata)
}
fun changeQuality(pos: Long) {
setSongURL()
initMediaFrom(pos)
}
private fun sendNotification() {
val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply {
setStyle(androidx.media.app.NotificationCompat.MediaStyle().setMediaSession(mediaSession.sessionToken).setShowActionsInCompactView(0, 1, 2))
setSmallIcon(R.drawable.icon)
setContentTitle(playlist[songIndex].title)
setContentText(playlist[songIndex].artist)
setLargeIcon(mediaSession.controller.metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART))
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// Adds transport controls
addAction(createAction(PREVIOUS_ACTION, R.drawable.back_noti, getString(R.string.previous)))
if (mediaController.playbackState.state == PlaybackStateCompat.STATE_PAUSED)
addAction(createAction(PLAY_ACTION, R.drawable.play_noti, getString(R.string.play)))
else
addAction(createAction(PAUSE_ACTION, R.drawable.pause_noti, getString(R.string.pause)))
addAction(createAction(NEXT_ACTION, R.drawable.forward_noti, getString(R.string.next)))
// creates the same kind of intent android creates to start the application so that the activity is resumed instead of recreated
val notificationIntent = Intent(applicationContext, MainActivity::class.java)
notificationIntent.action = Intent.ACTION_MAIN
notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER)
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setContentIntent(PendingIntent.getActivity(applicationContext, 2, notificationIntent, 0))
// prevents notification from being cancelled
setOngoing(true)
}
notificationManager.notify(1, builder.build())
}
private fun createAction(action: String, drawable: Int, title: String): NotificationCompat.Action {
val i = Intent(applicationContext, PlayerService::class.java)
i.action = action
val pi = PendingIntent.getService(applicationContext, 1, i, 0)
return NotificationCompat.Action.Builder(drawable, title, pi).build()
}
private fun songToMetaData(song: StreamingSong): MediaMetadataCompat.Builder {
metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, song.id)
metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist)
metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration.toLong())
metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, song.url)
val cursong = playlist[songIndex]
metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, cursong.img_url)
return metaDataBuilder
}
inner class LocalBinder : Binder() {
val service: PlayerService
get() = this@PlayerService
}
inner class Callback : MediaSessionCompat.Callback() {
override fun onPlay() {
if (ready) {
audioManager.requestAudioFocus(focusRequest)
handler.removeCallbacksAndMessages(null)
if (mediaPlayer == null) {
initMediaFrom(start)
} else if (mediaPlayer?.isPlaying == false) {
mediaPlayer?.start()
mediaSession.setPlaybackState(stateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, getCurrentPosition().toLong(), 1F).build())
}
}
}
override fun onPause() {
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.pause()
mediaSession.setPlaybackState(stateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, getCurrentPosition().toLong(), 0F).build())
handler.postDelayed({ mediaController.transportControls.stop()}, 180000)
}
}
override fun onStop() {
start += mediaPlayer!!.currentPosition.toLong()
mediaPlayer?.release()
mediaPlayer = null
ready = false
mediaSession.setPlaybackState(stateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, 0, 0F).build())
}
override fun onSkipToNext() {
nextMedia()
}
override fun onSkipToPrevious() {
// If mediaPlayer is null nothing happens
when {
mediaPlayer?.currentPosition ?: 4001 > 4000 -> mediaPlayer?.seekTo(0) // if current position == null then 4001, if > 4000 then seek to 0
randomSelection -> {
var ran = songIndex
while (ran == songIndex) ran = Random.nextInt(0, playlist.size - 1)
initMedia(playlist, ran)
}
loopPlaylist -> initMedia(playlist, (playlist.size + songIndex - 1) % playlist.size) // Kotlin module returns -1 instead of (size - 1)
else -> {
initMedia(playlist, maxOf(songIndex - 1, 0))
}
}
}
override fun onSeekTo(pos: Long) {
if (ready) {
if (pos >= start && pos <= (start + mediaPlayer!!.duration) * buffer / 100) {
mediaPlayer?.seekTo((pos - start).toInt())
mediaSession.sendSessionEvent("PositionChanged", null)
} else {
initMediaFrom(pos)
}
}
}
@SuppressLint("SwitchIntDef")
override fun onSetShuffleMode(shuffleMode: Int) {
when (shuffleMode) {
PlaybackStateCompat.SHUFFLE_MODE_ALL -> {
randomSelection = true
mediaSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL)
}
PlaybackStateCompat.SHUFFLE_MODE_NONE -> {
randomSelection = false
mediaSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE)
}
}
}
@SuppressLint("SwitchIntDef")
override fun onSetRepeatMode(repeatMode: Int) {
when (repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> {
mediaPlayer?.isLooping = false
loopPlaylist = true
loopSong = false
mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ALL)
}
PlaybackStateCompat.REPEAT_MODE_ONE -> {
mediaPlayer?.isLooping = true
loopPlaylist = false
loopSong = true
mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ONE)
}
PlaybackStateCompat.REPEAT_MODE_NONE -> {
mediaPlayer?.isLooping = false
loopPlaylist = false
loopSong = false
mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE)
}
}
}
}
inner class NotificationCallback : MediaControllerCompat.Callback() {
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
sendNotification()
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
sendNotification()
}
}
inner class FocusChangeListener : AudioManager.OnAudioFocusChangeListener {
override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> mediaController.transportControls.pause()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> mediaController.transportControls.pause()
AudioManager.AUDIOFOCUS_GAIN -> mediaController.transportControls.play()
}
}
}
}

View file

@ -0,0 +1,613 @@
package com.apollon
import android.os.AsyncTask
import android.util.Log
import com.apollon.classes.*
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONArray
import org.json.JSONObject
sealed class RequestResult {
class Ok(val result: JSONObject) : RequestResult()
class Error(val msg: String) : RequestResult()
}
fun makeRequest(m: Map<String, Any>): RequestResult {
val (user, pass) = Credentials.get()
val params = hashMapOf<String, Any>("user" to user,
"password" to pass)
m.forEach { (k, v) -> params[k] = v }
val data = JSONObject(params).toString()
try {
with(baseurl().openConnection() as HttpURLConnection) {
requestMethod = "POST"
this.outputStream.write(data.toByteArray())
this.outputStream.flush()
this.outputStream.close()
inputStream.bufferedReader().use {
val llines = it.lines().toArray()
assert(llines.count() == 1)
val j = JSONObject(llines[0] as String)
return if (j["response"] == "error")
RequestResult.Error(j["msg"] as String)
else
RequestResult.Ok(j)
}
}
} catch (e: Exception) {
return RequestResult.Error("Can't make the connection: " + e.message)
}
}
fun baseurl(): URL {
val (ip, snd) = Credentials.getServer()
val (proto, port) = snd
val split = ip.split('/')
val hostname = split[0]
val file = ip.substring(hostname.length, ip.length)
val protoString: String = when (proto) {
0 -> "http"
1 -> "https"
else -> {
assert(false); "https"
}
}
Log.e("GOTURL:", URL(protoString, hostname, port, file).toString())
return URL(protoString, hostname, port, file)
}
private class AllSongs(val listener: TaskListener) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Song>()
Log.e("HTTP", "request: all-songs")
when (val resp = makeRequest(hashMapOf("action" to "all-songs"))) {
is RequestResult.Ok -> {
val j = resp.result
val songs = j["values"] as JSONArray
for (i in 0 until songs.length()) {
val jsong = songs[i] as JSONObject
Log.e("AllSongs", jsong.toString())
val img = jsong["img"] as String
if (img == "")
ret.add(Song(jsong["uri"] as String, jsong["title"] as String, jsong["artist"] as String, i))
else
ret.add(Song(jsong["uri"] as String, jsong["title"] as String, jsong["artist"] as String, img, i))
}
val result = TaskResult.ServerSongsResult(ret)
Server.allSongs = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerSongsResult(error = resp.msg))
}
}
}
}
private class AllAlbums(val listener: TaskListener) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Playlist>()
Log.e("HTTP", "request: all-by-album")
when (val resp = makeRequest(hashMapOf("action" to "all-by-album"))) {
is RequestResult.Ok -> {
val j = resp.result
val values = j["values"] as JSONArray
for (i in 0 until values.length()) {
val album: JSONObject = values[i] as JSONObject
if ((album["img"] as String) != "")
ret.add(Playlist.Album(album["uri"] as String, album["title"] as String, album["img"] as String, album["#nsongs"] as Int))
else
ret.add(Playlist.Album(album["uri"] as String, album["title"] as String, album["#nsongs"] as Int))
}
val result = TaskResult.ServerPlaylistResult(ret)
Server.allAlbums = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerPlaylistResult(error = resp.msg))
}
}
}
}
private class AllArtists(val listener: TaskListener) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Playlist>()
when (val resp = makeRequest(hashMapOf("action" to "all-by-artist"))) {
is RequestResult.Ok -> {
val j = resp.result
val values = j["values"] as JSONArray
for (i in 0 until values.length()) {
val artist: JSONObject = values[i] as JSONObject
if ((artist["img"] as String) != "")
ret.add(Playlist.Artist(artist["name"] as String, artist["name"] as String, artist["img"] as String, artist["#albums"] as Int))
else
ret.add(Playlist.Artist(artist["name"] as String, artist["name"] as String, artist["#albums"] as Int))
}
val result = TaskResult.ServerPlaylistResult(ret)
Server.allArtists = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerPlaylistResult(error = resp.msg))
}
}
}
}
private class AllGenres(val listener: TaskListener) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Playlist>()
when (val resp = makeRequest(hashMapOf("action" to "all-by-genre"))) {
is RequestResult.Ok -> {
val j = resp.result
val values = j["values"] as JSONArray
for (i in 0 until values.length()) {
val genreName = (values[i] as JSONArray)[0].toString()
val items = (values[i] as JSONArray)[1].toString().toInt()
ret.add(Playlist.Genre(i.toString(), genreName, elements = items))
}
val result = TaskResult.ServerPlaylistResult(ret)
Server.allGenres = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerPlaylistResult(error = resp.msg))
}
}
}
}
private class AllPlaylists(val listener: TaskListener) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Playlist>()
when (val resp = makeRequest(hashMapOf("action" to "list-playlists"))) {
is RequestResult.Ok -> {
val j = resp.result
var img = ""
val values = j["result"] as JSONArray
for (i in 0 until values.length()) {
val playlist: JSONObject = values[i] as JSONObject
if (playlist["title"] != "Favourites") {
// checks if the first song in the playlist has an image URL
if ((playlist["uris"] as JSONArray).length() > 0) {
val firstImg = ((playlist["uris"] as JSONArray)[0] as JSONObject)["img"].toString()
img = if (firstImg.isNotEmpty())
firstImg
else
"playlist"
}
ret.add(Playlist.Custom(playlist["title"] as String, playlist["title"] as String, img, playlist["#nsongs"] as Int))
}
}
val result = TaskResult.ServerPlaylistResult(ret)
Server.allPlaylists = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerPlaylistResult(error = resp.msg))
}
}
}
}
private class SingleGenre(val listener: TaskListener, val name: String) : AsyncTask<Void, Int, Unit>() { // NOT WORKING
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Playlist>()
when (val resp = makeRequest(hashMapOf("action" to "genre", "key" to name))) {
is RequestResult.Ok -> {
val j = resp.result
val values = j["artists"] as JSONArray
for (i in 0 until values.length()) {
val artist: JSONObject = values[i] as JSONObject
if ((artist["img"] as String) != "")
ret.add(Playlist.Artist(artist["name"] as String, artist["name"] as String, artist["img"] as String, artist["#albums"] as Int))
else
ret.add(Playlist.Artist(artist["name"] as String, artist["name"] as String, artist["#albums"] as Int))
}
val result = TaskResult.ServerPlaylistResult(ret)
Server.genres[name] = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerSongsResult(error = resp.msg))
}
}
Log.e("HTTP", "Finished SingleGenre")
}
}
private class SingleAlbum(val listener: TaskListener, val uri: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Song>()
Log.e("HTTP", "request: single-album")
when (val resp = makeRequest(hashMapOf("action" to "album", "key" to uri))) {
is RequestResult.Ok -> {
val j = resp.result
val songs = (j["album"] as JSONObject)["songs"] as JSONArray
val artist = (j["album"] as JSONObject)["artist"] as String
val img = (j["album"] as JSONObject)["img"] as String
for (i in 0 until songs.length()) {
val jsong = songs[i] as JSONObject
val title: String = jsong["title"] as String
val uri = jsong["uri"] as String
if (img == "")
ret.add(Song(uri, title, artist, i))
else
ret.add(Song(uri, title, artist, img, i))
}
val result = TaskResult.ServerSongsResult(ret)
Server.albums[uri] = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerSongsResult(error = resp.msg))
}
}
}
}
private class SingleArtist(val listener: TaskListener, val name: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Playlist>()
Log.e("HTTP", "request: single-artist")
when (val resp = makeRequest(hashMapOf("action" to "artist", "key" to name))) {
is RequestResult.Ok -> {
val j = resp.result
val albums = (j["artist"] as JSONObject)["albums"] as JSONArray
for (i in 0 until albums.length()) {
val jalbum = albums[i] as JSONObject
val title = jalbum["title"] as String
val img = jalbum["img"] as String
val uri = jalbum["uri"] as String
val nsongs = jalbum["#nsongs"] as Int
if (img == "")
ret.add(Playlist.Album(uri, title, nsongs))
else
ret.add(Playlist.Album(uri, title, img, nsongs))
}
val result = TaskResult.ServerPlaylistResult(ret)
Server.artists[name] = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerPlaylistResult(error = resp.msg))
}
}
}
}
class SingleSong(private val uri: String) : AsyncTask<Void, Int, StreamingSong>() {
private var result: StreamingSong? = null
var error = ""
override fun doInBackground(vararg params: Void?): StreamingSong? {
Log.e("singleSong", uri)
Log.e("HTTP", "request: single-song")
val resp = makeRequest(hashMapOf("action" to "new-song", "quality" to Server.quality, "uri" to uri))
result = when (resp) {
is RequestResult.Ok -> {
val j = resp.result
val url = baseurl().toString().substring(0, baseurl().toString().length - 1) + (j["uri"] as String)
Log.e("URL", url)
val metadata = ((((j.get("metadata") as JSONObject).get("json") as JSONObject).get("media") as JSONObject)
.get("track") as JSONArray).get(0) as JSONObject
val title = if (metadata.has("Title")) metadata["Title"] as String else metadata["Album"] as String
val artist = metadata["Performer"] as String
val fduration = metadata["Duration"] as String
val duration = (fduration.toFloat() * 1000).toInt()
val s = StreamingSong(url, duration, uri, title, artist)
Log.e("HTTP", "new-song done")
s
}
is RequestResult.Error -> {
error = resp.msg; return null
}
}
return result
}
}
class SinglePlaylist(private val listener: TaskListener, val title: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
val ret = ArrayList<Song>()
Log.e("HTTP", "request: get-playlist")
when (val resp = makeRequest(hashMapOf("action" to "get-playlist", "title" to title))) {
is RequestResult.Ok -> {
val j = resp.result
val songs = (j["result"] as JSONObject)["uris"] as JSONArray
for (i in 0 until songs.length()) {
val jsong = songs[i] as JSONObject
val title: String = jsong["title"] as String
val artist: String = jsong["artist"] as String
val uri = jsong["uri"] as String
val img = jsong["img"] as String
if (img == "")
ret.add(Song(uri, title, artist, i))
else
ret.add(Song(uri, title, artist, img, i))
}
val result = TaskResult.ServerSongsResult(ret)
Server.playlists[title] = result
listener.onTaskCompleted(result)
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.ServerSongsResult(error = resp.msg))
}
}
}
}
class GetLyrics(private val listener: TaskListener, val artist: String, val title: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg params: Void?) {
lateinit var ret: List<String>
Log.e("HTTP", "request: lyrics")
when (val resp = makeRequest(hashMapOf("action" to "lyrics", "artist" to artist, "song" to title))) {
is RequestResult.Ok -> {
val j = resp.result
val content = j["lyrics"] as String
ret = content.split("\r\n")
listener.onTaskCompleted(TaskResult.LyricsResult(ret))
}
is RequestResult.Error -> {
listener.onTaskCompleted(TaskResult.LyricsResult(error = resp.msg))
}
}
Log.e("HTTP", "Finished GetLyrics")
}
}
class CreatePlaylist(private val listener: TaskListener, var title: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg p0: Void?) {
when (val resp = makeRequest(hashMapOf("action" to "new-playlist", "title" to title, "uris" to emptyList<String>()))) {
is RequestResult.Ok -> {
Server.resetPlaylists()
listener.onTaskCompleted(TaskResult.OperationResult("createPlaylist", title))
}
is RequestResult.Error -> listener.onTaskCompleted(TaskResult.OperationResult("createPlaylist", title, resp.msg))
}
Log.e("HTTP", "Finished CreatePlaylist")
}
}
class RemovePlaylist(private val listener: TaskListener, var title: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg p0: Void?) {
when (val resp = makeRequest(hashMapOf("action" to "remove-playlist", "title" to title))) {
is RequestResult.Ok -> {
Server.resetPlaylists()
Server.dropPlaylist(title)
listener.onTaskCompleted(TaskResult.OperationResult("removePlaylist", title))
}
is RequestResult.Error -> listener.onTaskCompleted(TaskResult.OperationResult("removePlaylist", title, resp.msg))
}
Log.e("HTTP", "Finished RemovePlaylist")
}
}
class RenamePlaylist(private val listener: TaskListener, private var oldTitle: String, private var newTitle: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg p0: Void?) {
when (val resp = makeRequest(hashMapOf("action" to "rename-playlist", "src" to oldTitle, "dst" to newTitle))) {
is RequestResult.Ok -> {
Server.resetPlaylists()
Server.dropPlaylist(oldTitle)
listener.onTaskCompleted(TaskResult.OperationResult("renamePlaylist", oldTitle))
}
is RequestResult.Error -> listener.onTaskCompleted(TaskResult.OperationResult("renamePlaylist", newTitle, resp.msg))
}
Log.e("HTTP", "Finished RemovePlaylist")
}
}
class AddSong(private val listener: TaskListener, var title: String, private var uri: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg p0: Void?) {
when (val resp = makeRequest(hashMapOf("action" to "modify-playlist", "playlist-action" to "add", "title" to title, "uris" to listOf(uri)))) {
is RequestResult.Ok -> {
Server.resetPlaylists()
Server.dropPlaylist(title)
listener.onTaskCompleted(TaskResult.OperationResult("addSong", title))
}
is RequestResult.Error -> listener.onTaskCompleted(TaskResult.OperationResult("addSong", title, resp.msg))
}
Log.e("HTTP", "Finished AddSong")
}
}
class RemoveSong(private val listener: TaskListener, var title: String, private var uri: String) : AsyncTask<Void, Int, Unit>() {
override fun doInBackground(vararg p0: Void?) {
when (val resp = makeRequest(hashMapOf("action" to "modify-playlist", "playlist-action" to "remove", "title" to title, "uris" to listOf(uri)))) {
is RequestResult.Ok -> {
Server.resetPlaylists()
Server.dropPlaylist(title)
listener.onTaskCompleted(TaskResult.OperationResult("removeSong", title))
}
is RequestResult.Error -> listener.onTaskCompleted(TaskResult.OperationResult("removeSong", title, resp.msg))
}
Log.e("HTTP", "Finished RemoveSong")
}
}
class DoLogin : AsyncTask<Void, Int, Boolean>() {
var result: Boolean = false
var done = false
var msg = ""
override fun doInBackground(vararg params: Void?): Boolean {
Log.e("HTTP", "request: do-login")
when (val resp = makeRequest(hashMapOf("action" to "challenge-login"))) {
is RequestResult.Ok -> result = true
is RequestResult.Error -> {
result = false; msg = resp.msg
}
}
done = true
return result
}
}
sealed class TaskResult {
class ServerPlaylistResult(val result: ArrayList<Playlist>? = null, val error: String = "") : TaskResult()
class ServerSongsResult(val result: ArrayList<Song>? = null, val error: String = "") : TaskResult()
class LyricsResult(val result: List<String>? = null, val error: String = "") : TaskResult()
class OperationResult(val task: String? = null, val title: String? = null, val error: String = "") : TaskResult()
}
object Server {
val artists = HashMap<String, TaskResult.ServerPlaylistResult>()
val albums = HashMap<String, TaskResult.ServerSongsResult>()
val genres = HashMap<String, TaskResult.ServerPlaylistResult>()
val playlists = HashMap<String, TaskResult.ServerSongsResult>()
var allSongs: TaskResult.ServerSongsResult? = null
var allAlbums: TaskResult.ServerPlaylistResult? = null
var allGenres: TaskResult.ServerPlaylistResult? = null
var allArtists: TaskResult.ServerPlaylistResult? = null
var allPlaylists: TaskResult.ServerPlaylistResult? = null
var quality = "high"
fun getArtist(listener: TaskListener, id: String) {
if (artists.containsKey(id)) {
listener.onTaskCompleted(artists[id] as TaskResult)
} else {
SingleArtist(listener, id).execute()
}
}
fun getGenre(listener: TaskListener, id: String) {
Log.e("singlegenre", id)
if (genres.containsKey(id)) {
listener.onTaskCompleted(genres[id] as TaskResult)
} else {
SingleGenre(listener, id).execute()
}
}
fun getAlbum(listener: TaskListener, id: String) {
Log.e("album_id", id)
if (albums.containsKey(id)) {
listener.onTaskCompleted(albums[id] as TaskResult)
} else {
SingleAlbum(listener, id).execute()
}
}
fun getSongs(listener: TaskListener) {
if (allSongs == null) {
AllSongs(listener).execute()
} else {
listener.onTaskCompleted(allSongs!!)
}
}
fun getAlbums(listener: TaskListener) {
if (allAlbums == null) {
AllAlbums(listener).execute()
} else {
listener.onTaskCompleted(allAlbums!!)
}
}
fun getArtists(listener: TaskListener) {
if (allArtists == null) {
AllArtists(listener).execute()
} else {
listener.onTaskCompleted(allArtists!!)
}
}
fun getGenres(listener: TaskListener) {
if (allGenres == null) {
AllGenres(listener).execute()
} else {
listener.onTaskCompleted(allGenres!!)
}
}
fun getLyrics(listener: TaskListener, artist: String, title: String) {
GetLyrics(listener, artist, title).execute()
}
fun getPlaylists(listener: TaskListener) {
if (allPlaylists == null) {
AllPlaylists(listener).execute()
} else {
listener.onTaskCompleted(allPlaylists!!)
}
}
fun getPlaylist(listener: TaskListener, title: String) {
if (playlists.containsKey(title)) {
listener.onTaskCompleted(playlists[title] as TaskResult)
} else {
SinglePlaylist(listener, title).execute()
}
}
fun createPlaylist(listener: TaskListener, title: String) {
CreatePlaylist(listener, title).execute()
}
fun removePlaylist(listener: TaskListener, title: String) {
RemovePlaylist(listener, title).execute()
}
fun renamePlaylist(listener: TaskListener, oldTitle: String, newTitle: String) {
RenamePlaylist(listener, oldTitle, newTitle).execute()
}
fun addSong(listener: TaskListener, title: String, uri: String) {
AddSong(listener, title, uri).execute()
}
fun removeSong(listener: TaskListener, title: String, uri: String) {
RemoveSong(listener, title, uri).execute()
}
fun resetPlaylists() {
allPlaylists = null
}
fun dropPlaylist(title: String) {
playlists.remove(title)
}
}

View file

@ -0,0 +1,5 @@
package com.apollon
interface TaskListener {
fun onTaskCompleted(result: TaskResult)
}

View file

@ -0,0 +1,196 @@
@file:Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
package com.apollon.adapters
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.apollon.*
import com.apollon.classes.Playlist
import com.apollon.fragments.PlayListsFragment
import com.apollon.fragments.SongsFragment
import com.squareup.picasso.Picasso
import java.util.*
import kotlin.collections.ArrayList
class PlaylistAdapter(var playlists: ArrayList<Playlist>, private val context: Context, private val fragment: PlayListsFragment) : RecyclerView.Adapter<PlaylistViewHolder>(), Filterable, TaskListener {
private val filter = PlaylistFilter()
private var filteredPlaylists = playlists
// Gets the number of playlists in the list
override fun getItemCount(): Int {
return filteredPlaylists.size
}
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistViewHolder {
return PlaylistViewHolder(LayoutInflater.from(context).inflate(R.layout.playlist_card, parent, false))
}
// Binds each playlist in the ArrayList to a view
@SuppressLint("InflateParams")
override fun onBindViewHolder(holder: PlaylistViewHolder, position: Int) {
val playlist = filteredPlaylists[position]
// sets the correct title
when (playlist) {
is Playlist.AllSongs -> holder.title.text = context.getString(R.string.all)
is Playlist.AllArtists -> holder.title.text = context.getString(R.string.artists)
is Playlist.AllAlbums -> holder.title.text = context.getString(R.string.albums)
is Playlist.AllGenres -> holder.title.text = context.getString(R.string.genres)
is Playlist.AllPlaylists -> holder.title.text = context.getString(R.string.playlists)
is Playlist.Favourites -> holder.title.text = context.getString(R.string.Favourites)
else -> {
holder.title.text = playlist.title
holder.title.isSelected = true
holder.elements.text = String.format(context.getString(R.string.elements), playlist.elements)
}
}
// Loads in the correct image
when (playlist.img_url) {
"all" -> holder.thumbnail.setImageBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.all))
"artist" -> holder.thumbnail.setImageBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.artist))
"album" -> holder.thumbnail.setImageBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.album))
"genre" -> holder.thumbnail.setImageBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.genre))
"favourites" -> holder.thumbnail.setImageBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.favourites))
"playlist" -> holder.thumbnail.setImageBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.playlist))
else -> Picasso.get().load(playlist.img_url).into(holder.thumbnail)
}
// CardView listener
val target = when (playlist) {
is Playlist.AllSongs -> SongsFragment.newInstance(playlist)
is Playlist.Album -> SongsFragment.newInstance(playlist)
is Playlist.Custom -> SongsFragment.newInstance(playlist)
is Playlist.Favourites -> SongsFragment.newInstance(playlist)
else -> PlayListsFragment.newInstance(playlist)
}
holder.itemView.setOnClickListener { (context as MainActivity).replaceFragment(target as Fragment) }
// Custom playlist buttons
if (playlist is Playlist.Custom) {
val deleteButton = holder.itemView.findViewById<Button>(R.id.button_delete)
val editButton = holder.itemView.findViewById<Button>(R.id.button_edit)
deleteButton.visibility = View.VISIBLE
editButton.visibility = View.VISIBLE
// delete click listener
deleteButton.setOnClickListener {
// creates alert
AlertDialog.Builder(context, R.style.AlertStyle)
.setTitle(context.getString(R.string.delete_title))
.setMessage(context.getString(R.string.delete_message) + " ${playlist.title}?")
.setPositiveButton(context.getString(R.string.delete)) { dialog, _ ->
Server.removePlaylist(this, playlist.title)
dialog.dismiss()
}
.setNegativeButton(context.getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
// edit click listener
editButton.setOnClickListener {
val editView = LayoutInflater.from(context).inflate(R.layout.modify, null)
val editText = editView.findViewById<EditText>(R.id.edit_title)
editText.setText(playlist.title)
// creates alert
AlertDialog.Builder(context, R.style.AlertStyle)
.setView(editView)
.setTitle(context.getString(R.string.edit_title))
.setMessage(context.getString(R.string.edit_playlist_message))
.setPositiveButton(context.getString(R.string.edit)) { dialog, _ ->
Server.renamePlaylist(this, playlist.title, editText.text.toString())
dialog.dismiss()
}
.setNegativeButton(context.getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
}
}
override fun onTaskCompleted(result: TaskResult) {
if (result is TaskResult.OperationResult) {
when (result.task) {
"removePlaylist" ->
if (result.error == "")
(context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.playlist_deleted, result.title), Toast.LENGTH_SHORT).show()
context.refreshFragment(fragment)
}
else
(context as MainActivity).runOnUiThread {
Toast.makeText(context, "${context.getString(R.string.error)}: ${result.error}", Toast.LENGTH_SHORT).show()
}
"renamePlaylist" ->
when {
result.error == "" ->
(context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.playlist_renamed, result.title), Toast.LENGTH_SHORT).show()
context.refreshFragment(fragment)
}
result.error.contains("same title") ->
(context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.playlist_rename_fail, result.title), Toast.LENGTH_SHORT).show()
context.refreshFragment(fragment)
}
else ->
(context as MainActivity).runOnUiThread {
Toast.makeText(context, "${context.getString(R.string.error)}: ${result.error}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
override fun getFilter(): Filter {
return filter
}
inner class PlaylistFilter : Filter() {
override fun performFiltering(s: CharSequence?): FilterResults {
val res = FilterResults()
if (s.isNullOrEmpty())
res.values = playlists
else {
val resList = ArrayList<Playlist>()
playlists.forEach {
if (it.title.toLowerCase(Locale.ROOT).contains(s.toString().toLowerCase(Locale.ROOT)))
resList.add(it)
}
res.values = resList
}
return res
}
override fun publishResults(s: CharSequence?, r: FilterResults?) {
filteredPlaylists = r?.values as ArrayList<Playlist>
notifyDataSetChanged()
}
}
}
class PlaylistViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title = view.findViewById(R.id.title) as TextView
val thumbnail = view.findViewById(R.id.thumbnail) as ImageView
val elements = view.findViewById(R.id.elements) as TextView
}

View file

@ -0,0 +1,179 @@
@file:Suppress("UNCHECKED_CAST")
package com.apollon.adapters
import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import com.apollon.*
import com.apollon.classes.PlaylistSong
import com.apollon.classes.Song
import com.apollon.fragments.PlayerFragment
import com.apollon.fragments.SongsFragment
import com.squareup.picasso.Picasso
import java.util.*
class SongAdapter(private val playlistTitle: String, var songs: ArrayList<Song>, private val context: Context, private val fragment: SongsFragment) : RecyclerView.Adapter<SongViewHolder>(), TaskListener, Filterable {
private val filter = SongFilter()
private var filteredSongs = songs
private lateinit var selectedView: View
private lateinit var selectedSong: Song
// Gets the number of songs in the list
override fun getItemCount(): Int {
return filteredSongs.size
}
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
return SongViewHolder(LayoutInflater.from(context).inflate(R.layout.song_card, parent, false))
}
// Binds each `(activity as MainActivity).currentSong` in the ArrayList to a view
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val song = filteredSongs[position]
holder.title.text = song.title
holder.artist.text = song.artist
holder.title.isSelected = true
holder.artist.isSelected = true
// Thumbnail download
if (song.img_url.isNotEmpty())
Picasso.get().load(song.img_url).into(holder.thumbnail)
// Menu listener
holder.menu.setOnClickListener { showPopUpMenu(it, song) }
// CardView listener
holder.itemView.setOnClickListener {
(context as MainActivity).replaceFragment(PlayerFragment())
context.player.initMedia(songs, filteredSongs[position].index)
}
}
private fun showPopUpMenu(view: View, song: Song) {
val popupMenu = PopupMenu(context, view)
val inflater = popupMenu.menuInflater
inflater.inflate(R.menu.song_menu, popupMenu.menu)
if (song is PlaylistSong)
popupMenu.menu.findItem(R.id.action_remove).isVisible = true
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_add_favourite -> Server.addSong(this, "Favourites", song.id)
R.id.action_add_playlist -> {
selectedView = view
selectedSong = song
Server.getPlaylists(this)
}
R.id.action_remove -> {
Server.removeSong(this, playlistTitle, song.id)
}
}
true
}
}
override fun getFilter(): Filter {
return filter
}
override fun onTaskCompleted(result: TaskResult) {
when (result) {
is TaskResult.ServerPlaylistResult -> {
if (result.error == "") {
val menu = PopupMenu(context, selectedView)
result.result?.forEach {
menu.menu.add(it.title)
}
menu.setOnMenuItemClickListener { item ->
Server.addSong(this, item.title.toString(), selectedSong.id)
true
}
(context as Activity).runOnUiThread {
menu.show()
}
} else
(context as Activity).runOnUiThread {
Toast.makeText(context, "${context.getString(R.string.error)}: ${result.error}", Toast.LENGTH_LONG).show()
}
}
is TaskResult.OperationResult -> {
when (result.task) {
"addSong" -> {
when {
result.error == "" ->
if (result.title == "Favourites") {
(context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.song_added, context.getString(R.string.Favourites)), Toast.LENGTH_SHORT).show()
}
} else (context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.song_added, result.title), Toast.LENGTH_SHORT).show()
}
result.error.contains("already in the playlist") -> {
(context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.already_in, result.title), Toast.LENGTH_SHORT).show()
}
}
else -> (context as MainActivity).runOnUiThread {
Toast.makeText(context, "${context.getString(R.string.error)}: ${result.error}", Toast.LENGTH_SHORT).show()
}
}
}
"removeSong" -> {
if (result.error == "")
(context as MainActivity).runOnUiThread {
Toast.makeText(context, context.getString(R.string.song_removed, result.title), Toast.LENGTH_SHORT).show()
Server.getPlaylist(fragment, playlistTitle)
}
else
(context as MainActivity).runOnUiThread {
Toast.makeText(context, "${context.getString(R.string.error)}: ${result.error}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
inner class SongFilter : Filter() {
override fun performFiltering(s: CharSequence?): FilterResults {
val res = FilterResults()
if (s.isNullOrEmpty())
res.values = songs
else {
val resList = ArrayList<Song>()
val query = s.toString().toLowerCase(Locale.ROOT)
songs.forEach {
if (it.title.toLowerCase(Locale.ROOT).contains(query) or it.artist.toLowerCase(Locale.ROOT).contains(query))
resList.add(it)
}
res.values = resList
}
return res
}
override fun publishResults(s: CharSequence?, r: FilterResults?) {
filteredSongs = r?.values as ArrayList<Song>
notifyDataSetChanged()
}
}
}
class SongViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title = view.findViewById(R.id.title) as TextView
val artist = view.findViewById(R.id.artist) as TextView
val thumbnail = view.findViewById(R.id.thumbnail) as ImageView
val menu = view.findViewById(R.id.menu) as ImageView
}

View file

@ -0,0 +1,42 @@
@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
package com.apollon.classes
import android.content.Context
object Credentials {
lateinit var server: String
lateinit var user: String
private lateinit var password: String
private var proto: Int = -1
private var port: Int = -1
fun init(context: Context) {
val prefs = context.getSharedPreferences("Apollon", 0)
user = prefs.getString("user", "")
password = prefs.getString("password", "")
server = prefs.getString("server", "")
proto = prefs.getInt("protocol", 1)
port = prefs.getInt("port", 80)
}
fun get(): Pair<String, String> {
return Pair(user, password)
}
fun getServer(): Pair<String, Pair<Int, Int>> {
return Pair(server, Pair(proto, port))
}
fun save(context: Context, user: String, password: String, server: String, proto: Int, port: Int) {
val prefs = context.getSharedPreferences("Apollon", 0)
val editor = prefs.edit()
editor.putString("user", user)
editor.putInt("protocol", proto)
editor.putString("password", password)
editor.putString("server", server)
editor.putInt("port", port)
editor.apply()
init(context) // reinstate variables
}
}

View file

@ -0,0 +1,25 @@
package com.apollon.classes
import java.io.Serializable
sealed class Playlist(val id: String, val title: String, val img_url: String, val elements: Int) : Serializable {
class Begin : Playlist("begin", "start screen", "https://cdn3.iconfinder.com/data/icons/66-cds-dvds/512/Icon_60-512.png", 0)
class AllSongs : Playlist("AllSongs", "", "all", 0)
class AllAlbums : Playlist("AllAlbums", "", "album", 0)
class AllArtists : Playlist("AllArtists", "", "artist", 0)
class AllGenres : Playlist("AllGenres", "", "genre", 0)
class AllPlaylists : Playlist("AllPlaylists", "", "playlist", 0)
class Favourites : Playlist("Favourites", "Favourites", "favourites", 0)
class Artist(id: String, title: String, img_url: String, elements: Int) : Playlist(id, title, img_url, elements) {
constructor(id: String, title: String, elements: Int) : this(id, title, "artist", elements)
}
class Album(id: String, title: String, img_url: String, elements: Int) : Playlist(id, title, img_url, elements) {
constructor(id: String, title: String, elements: Int) : this(id, title, "album", elements)
}
class Genre(id: String, title: String, img_url: String, elements: Int) : Playlist(id, title, img_url, elements) {
constructor(id: String, title: String, elements: Int) : this(id, title, "genre", elements)
}
class Custom(id: String, title: String, img: String, elements: Int) : Playlist(id, title, img, elements)
}

View file

@ -0,0 +1,21 @@
package com.apollon.classes
import java.io.Serializable
open class Song(val id: String, val title: String, val artist: String, var img_url: String, val index: Int) : Serializable {
constructor(id: String, title: String, artist: String, index: Int) :
this(id, title, artist, "", index)
override fun equals(other: Any?): Boolean {
if (other !is Song)
return false
return this.id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
class PlaylistSong(id: String, title: String, artist: String, img_url: String, index: Int) : Song(id, title, artist, img_url, index)

View file

@ -0,0 +1,7 @@
package com.apollon.classes
class StreamingSong(val url: String, val duration: Int, id: String, title: String, artist: String, img_url: String) :
Song(id, title, artist, img_url, 0) {
constructor(url: String, duration: Int, id: String, title: String, artist: String) :
this(url, duration, id, title, artist, "")
}

View file

@ -0,0 +1,90 @@
package com.apollon.fragments
import android.content.Context
import android.content.Context.*
import android.net.ConnectivityManager
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.Toast
import androidx.core.content.ContextCompat.getSystemService
import androidx.fragment.app.Fragment
import com.apollon.DoLogin
import com.apollon.MainActivity
import com.apollon.R
import com.apollon.classes.Credentials
class LoginFragment : Fragment() {
private lateinit var mView: View
private lateinit var loginButton: Button
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Credentials.init(context!!)
mView = inflater.inflate(R.layout.login, container, false)
val (user, password) = Credentials.get()
val userField: EditText = mView.findViewById(R.id.input_username)
val passField: EditText = mView.findViewById(R.id.input_password)
userField.text?.clear()
userField.text?.insert(0, user)
passField.text?.clear()
passField.text?.insert(0, password)
val (server, snd) = Credentials.getServer()
val (proto, port) = snd
val serverField: EditText = mView.findViewById(R.id.input_ip)
val protocolField: Spinner = mView.findViewById(R.id.protocol)
val portField: EditText = mView.findViewById(R.id.input_port)
serverField.text?.clear()
serverField.text?.insert(0, server)
portField.text?.clear()
portField.text?.insert(0, port.toString())
protocolField.setSelection(proto)
loginButton = mView.findViewById(R.id.login_btn)
loginButton.setOnClickListener {
val newUser = userField.text.toString()
val newPass = passField.text.toString()
val newPort = portField.text.toString().toInt()
val newServer = serverField.text.toString()
val newProto = protocolField.selectedItemId.toInt()
Credentials.save(context!!, newUser, newPass, newServer, newProto, newPort)
if(!isNetworkAvailable()) {
val msg = R.string.no_connectivity
Toast.makeText(context!!, msg, Toast.LENGTH_LONG).show()
} else {
val l = DoLogin()
l.execute()
while (!l.done) {
Log.e("Trying login", l.done.toString())
}
if (!l.result) {
Toast.makeText(context!!, l.msg, Toast.LENGTH_LONG).show()
Log.e("Login", "Invalid Login")
} else {
(activity as MainActivity).replaceFragment(PlayListsFragment(), false)
}
}
}
return mView
}
private fun isNetworkAvailable(): Boolean {
// Used to check if the phone can starts connections
val connectivityManager =
activity?.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager?
val activeNetworkInfo = connectivityManager!!.activeNetworkInfo
return activeNetworkInfo != null && activeNetworkInfo.isConnected
}
}

View file

@ -0,0 +1,185 @@
package com.apollon.fragments
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.SearchView
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.apollon.*
import com.apollon.adapters.PlaylistAdapter
import com.apollon.classes.Playlist
class PlayListsFragment : Fragment(), TaskListener, View.OnClickListener {
private val playlists: ArrayList<Playlist> = ArrayList()
lateinit var playlist: Playlist
lateinit var recyclerView: RecyclerView
private lateinit var selectedPlaylist: String
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.e("PlaylistFragment", "OnCreateView")
val mView = inflater.inflate(R.layout.playlists, container, false)
val search = mView.findViewById<SearchView>(R.id.search)
recyclerView = mView.findViewById(R.id.recycler_view)
playlist = if (this.arguments != null && this.arguments!!.containsKey("playlist"))
this.arguments!!.get("playlist") as Playlist
else {
search.isVisible = false
Playlist.Begin()
}
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(s: String?): Boolean {
return false
}
override fun onQueryTextChange(s: String?): Boolean {
(recyclerView.adapter as PlaylistAdapter).filter.filter(s)
return false
}
})
if (playlist is Playlist.AllPlaylists) {
val addButton = mView.findViewById<Button>(R.id.new_playlist_button)
addButton.setOnClickListener(this)
addButton.visibility = View.VISIBLE
}
// Creates a Grid Layout Manager
recyclerView.layoutManager = GridLayoutManager(context, 1)
// Access the RecyclerView Adapter and load the data into it
recyclerView.adapter = PlaylistAdapter(playlists, requireContext(), this)
// Loads elements into the ArrayList
addPlaylists()
return mView
}
// New playlist button
@SuppressLint("InflateParams")
override fun onClick(v: View?) {
when (v?.id) {
R.id.new_playlist_button -> {
val editView = LayoutInflater.from(context).inflate(R.layout.modify, null)
// creates alert
AlertDialog.Builder(context, R.style.AlertStyle)
.setTitle(getString(R.string.new_playlist))
.setMessage(getString(R.string.edit_playlist_message))
.setView(editView)
.setPositiveButton(getString(R.string.create)) { dialog, _ ->
selectedPlaylist = editView.findViewById<EditText>(R.id.edit_title).text.toString()
Server.createPlaylist(this, selectedPlaylist)
dialog.dismiss()
}
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
}
}
// Adds playlists to the empty ArrayList
private fun addPlaylists() {
playlists.clear()
when (playlist) {
is Playlist.Begin -> {
playlists.add(Playlist.AllSongs())
playlists.add(Playlist.AllArtists())
playlists.add(Playlist.AllAlbums())
playlists.add(Playlist.AllGenres())
playlists.add(Playlist.AllPlaylists())
playlists.add(Playlist.Favourites())
return // break
}
is Playlist.Artist -> Server.getArtist(this, playlist.id)
is Playlist.Album -> {
assert(false); return
}
is Playlist.AllSongs -> {
assert(false); return
}
is Playlist.Genre -> Server.getGenre(this, playlist.title)
is Playlist.AllAlbums -> Server.getAlbums(this)
is Playlist.AllArtists -> Server.getArtists(this)
is Playlist.AllGenres -> Server.getGenres(this)
is Playlist.AllPlaylists -> Server.getPlaylists(this)
is Playlist.Favourites -> {
assert(false); return
}
is Playlist.Custom -> {
assert(false); return
} // this should have been forwarded to SongsFragments
}
}
override fun onTaskCompleted(result: TaskResult) {
when (result) {
is TaskResult.ServerPlaylistResult -> {
if (result.error == "") {
playlists.clear()
result.result?.forEach { playlists.add(it) }
// Access the RecyclerView Adapter and load the data into it
activity?.runOnUiThread {
(recyclerView.adapter as PlaylistAdapter).playlists = playlists
(recyclerView.adapter as PlaylistAdapter).notifyDataSetChanged()
}
} else
activity?.runOnUiThread {
Toast.makeText(context, result.error, Toast.LENGTH_LONG).show()
}
}
is TaskResult.OperationResult -> {
when (result.task) {
"createPlaylist" -> {
when {
result.error == "" -> {
activity?.runOnUiThread {
Toast.makeText(context, "Playlist $selectedPlaylist created", Toast.LENGTH_SHORT).show()
}
Server.getPlaylists(this)
}
result.error.contains("There is a playlist with the same title and user already") -> {
activity?.runOnUiThread {
Toast.makeText(context, context!!.getString(R.string.already_exists, selectedPlaylist), Toast.LENGTH_SHORT).show()
}
}
else -> activity?.runOnUiThread {
Toast.makeText(context, "Error: ${result.error}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}
companion object {
fun newInstance(playlist: Playlist): PlayListsFragment {
val args = Bundle()
args.putSerializable("playlist", playlist)
val fragment = PlayListsFragment()
fragment.arguments = args
return fragment
}
}
}

View file

@ -0,0 +1,456 @@
@file:Suppress("PrivatePropertyName")
package com.apollon.fragments
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.os.Handler
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.view.*
import android.widget.*
import androidx.fragment.app.Fragment
import com.apollon.*
import kotlin.math.abs
class PlayerFragment : Fragment(), TaskListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
private lateinit var callback: Callback
private var seekBarHandler = Handler()
lateinit var albumArt: ImageView
lateinit var title: TextView
lateinit var artist: TextView
lateinit var seekBar: SeekBar
private lateinit var currentTime: TextView
lateinit var duration: TextView
lateinit var playButton: Button
lateinit var loopButton: Button
lateinit var randomButton: Button
private lateinit var favouriteButton: Button
private lateinit var qualityButton: Button
private lateinit var gestureDetector: GestureDetector
private var isFavourite: Boolean = false
var songDuration = 0
var songUri = ""
@SuppressLint("SourceLockedOrientationActivity", "ClickableViewAccessibility")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
(activity as MainActivity).miniPlayer.visibility = View.GONE
// Initialize gesture detection
gestureDetector = GestureDetector((activity as MainActivity).applicationContext, GListener())
val mView = inflater.inflate(R.layout.player, container, false)
title = mView.findViewById(R.id.song_title)
artist = mView.findViewById(R.id.song_artist)
title.isSelected = true
artist.isSelected = true
albumArt = mView.findViewById(R.id.album_art)
seekBar = mView.findViewById(R.id.seekbar_audio)
currentTime = mView.findViewById(R.id.current_position)
duration = mView.findViewById(R.id.duration)
playButton = mView.findViewById(R.id.button_play)
loopButton = mView.findViewById(R.id.button_repeat)
randomButton = mView.findViewById(R.id.button_random)
favouriteButton = mView.findViewById(R.id.button_favourite)
qualityButton = mView.findViewById(R.id.button_quality)
// quality button look
when (Server.quality) {
"high" ->
if (qualityButton.tag == "inverted")
qualityButton.setBackgroundResource(R.drawable.hq_button_selector_inverted)
else
qualityButton.setBackgroundResource(R.drawable.hq_button_selector)
"medium" ->
if (qualityButton.tag == "inverted")
qualityButton.setBackgroundResource(R.drawable.mq_button_selector_inverted)
else
qualityButton.setBackgroundResource(R.drawable.mq_button_selector)
"low" ->
if (qualityButton.tag == "inverted")
qualityButton.setBackgroundResource(R.drawable.lq_button_selector_inverted)
else
qualityButton.setBackgroundResource(R.drawable.lq_button_selector)
}
mView.findViewById<Button>(R.id.button_previous).setOnClickListener(activity as MainActivity)
mView.findViewById<Button>(R.id.button_next).setOnClickListener(activity as MainActivity)
mView.findViewById<Button>(R.id.button_share).setOnClickListener(this)
mView.findViewById<Button>(R.id.button_lyrics).setOnClickListener(this)
playButton.setOnClickListener(activity as MainActivity)
loopButton.setOnClickListener(this)
randomButton.setOnClickListener(this)
seekBar.setOnSeekBarChangeListener(this)
favouriteButton.setOnClickListener(this)
qualityButton.setOnClickListener(this)
albumArt.setOnTouchListener { _, e -> gestureDetector.onTouchEvent(e) }
callback = Callback()
(activity as MainActivity).mediaController.registerCallback(callback)
return mView
}
override fun onDestroyView() {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
seekBarHandler.removeCallbacksAndMessages(null)
(activity as MainActivity).mediaController.unregisterCallback(callback)
(activity as MainActivity).miniPlayer.visibility = View.VISIBLE
super.onDestroyView()
}
private fun millisToString(millis: Int): String {
val seconds = millis / 1000
return "" + seconds / 60 + ":" + String.format("%02d", seconds % 60) // 2 digits precision - 0 for padding
}
override fun onProgressChanged(seekbar: SeekBar?, progress: Int, fromUser: Boolean) {
currentTime.text = millisToString(songDuration * progress / 1000)
}
override fun onStartTrackingTouch(seekbar: SeekBar?) {
seekBarHandler.removeCallbacksAndMessages(null)
}
override fun onStopTrackingTouch(seekbar: SeekBar?) {
(activity as MainActivity).mediaController.transportControls.seekTo(songDuration.toLong() * seekbar!!.progress / 1000)
seekBar.isEnabled = false
}
private fun updateSeekBar() {
val currentPosition = (activity as MainActivity).player.getCurrentPosition()
seekBar.progress = currentPosition * 1000 / songDuration
currentTime.text = millisToString(currentPosition)
}
private fun startSeekBarHandler() {
seekBarHandler.removeCallbacksAndMessages(null)
activity?.runOnUiThread(object : Runnable {
override fun run() {
updateSeekBar()
seekBarHandler.postDelayed(this, 1000)
}
})
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.button_repeat ->
when ((activity as MainActivity).mediaController.repeatMode) {
// Loops playlist
PlaybackStateCompat.REPEAT_MODE_NONE -> {
(activity as MainActivity).mediaController.transportControls.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ALL)
}
// Loops current song
PlaybackStateCompat.REPEAT_MODE_ALL -> {
(activity as MainActivity).mediaController.transportControls.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ONE)
}
// Doesn't loop
PlaybackStateCompat.REPEAT_MODE_ONE -> {
(activity as MainActivity).mediaController.transportControls.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE)
}
}
R.id.button_random ->
if ((activity as MainActivity).mediaController.shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL) {
(activity as MainActivity).mediaController.transportControls.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE)
} else {
(activity as MainActivity).mediaController.transportControls.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL)
}
R.id.button_favourite -> {
favouriteOps()
}
R.id.button_share -> {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_message_start) + " ${artist.text} - ${title.text} " + getString(R.string.share_message_end))
type = "text/plain"
}
startActivity(sendIntent)
}
R.id.button_lyrics -> Server.getLyrics(this, artist.text.toString(), title.text.toString())
R.id.button_quality -> {
val popupMenu = PopupMenu(context, v)
val inflater = popupMenu.menuInflater
inflater.inflate(R.menu.quality_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.low_quality -> {
if (Server.quality != "low") {
Server.quality = "low"
(activity as MainActivity).player.changeQuality(seekBar.progress.toLong() * songDuration / 1000)
if (qualityButton.tag == "inverted")
qualityButton.setBackgroundResource(R.drawable.lq_button_selector_inverted)
else
qualityButton.setBackgroundResource(R.drawable.lq_button_selector)
}
}
R.id.medium_quality -> {
if (Server.quality != "medium") {
Server.quality = "medium"
(activity as MainActivity).player.changeQuality(seekBar.progress.toLong() * songDuration / 1000)
if (qualityButton.tag == "inverted")
qualityButton.setBackgroundResource(R.drawable.mq_button_selector_inverted)
else
qualityButton.setBackgroundResource(R.drawable.mq_button_selector)
}
}
R.id.high_quality -> {
if (Server.quality != "high") {
Server.quality = "high"
(activity as MainActivity).player.changeQuality(seekBar.progress.toLong() * songDuration / 1000)
if (qualityButton.tag == "inverted")
qualityButton.setBackgroundResource(R.drawable.hq_button_selector_inverted)
else
qualityButton.setBackgroundResource(R.drawable.hq_button_selector)
}
}
}
true
}
}
}
}
fun favouriteOps() {
val songId = (activity as MainActivity).mediaController.metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
if (isFavourite) {
Server.removeSong(this, "Favourites", songId)
} else {
Server.addSong(this, "Favourites", songId)
}
}
override fun onTaskCompleted(result: TaskResult) {
when (result) {
is TaskResult.ServerSongsResult -> {
if (result.error == "") {
isFavourite = result.result!!.any { it.id == songUri }
activity?.runOnUiThread {
if (isFavourite)
if (favouriteButton.tag == "inverted") {
favouriteButton.setBackgroundResource(R.drawable.favourite_button_selector_inverted)
} else
favouriteButton.setBackgroundResource(R.drawable.favourite_button_selector)
else if (favouriteButton.tag == "inverted")
favouriteButton.setBackgroundResource(R.drawable.favourite_not_button_selector_inverted)
else
favouriteButton.setBackgroundResource(R.drawable.favourite_not_button_selector)
}
} else
activity?.runOnUiThread {
Toast.makeText(context, result.error, Toast.LENGTH_LONG).show()
}
}
is TaskResult.LyricsResult -> {
if (result.error == "") {
var st = result.result!!.joinToString(separator = "\n")
if (st == "")
st = resources.getString(R.string.lyrics)
activity?.runOnUiThread {
AlertDialog.Builder(context, R.style.AlertStyle)
.setTitle(title.text)
.setMessage(st)
.setNegativeButton(context?.getString(R.string.close)) { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
}
}
is TaskResult.OperationResult -> {
when (result.task) {
"removeSong" -> {
if (result.error == "") {
if (favouriteButton.tag == "inverted")
favouriteButton.setBackgroundResource(R.drawable.favourite_not_button_selector_inverted)
else
favouriteButton.setBackgroundResource(R.drawable.favourite_not_button_selector)
isFavourite = false
activity?.runOnUiThread {
Toast.makeText(context, context!!.getString(R.string.song_removed, context!!.getString(R.string.favourites)), Toast.LENGTH_SHORT).show()
}
} else
activity?.runOnUiThread {
Toast.makeText(context, "${context!!.getString(R.string.error)}: ${result.error}", Toast.LENGTH_SHORT).show()
}
}
"addSong" -> {
if (result.error == "") {
if (favouriteButton.tag == "inverted")
favouriteButton.setBackgroundResource(R.drawable.favourite_button_selector_inverted)
else
favouriteButton.setBackgroundResource(R.drawable.favourite_button_selector)
isFavourite = true
activity?.runOnUiThread {
Toast.makeText(context, context!!.getString(R.string.song_added, context!!.getString(R.string.favourites)), Toast.LENGTH_SHORT).show()
}
} else activity?.runOnUiThread {
Toast.makeText(context, "${context!!.getString(R.string.error)}: ${result.error}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
inner class Callback : MediaControllerCompat.Callback() {
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
super.onPlaybackStateChanged(state)
when (state?.state) {
PlaybackStateCompat.STATE_PLAYING -> {
seekBar.isEnabled = true
playButton.setBackgroundResource(R.drawable.pause_button_selector)
startSeekBarHandler()
}
PlaybackStateCompat.STATE_PAUSED -> {
playButton.setBackgroundResource(R.drawable.play_button_selector)
seekBarHandler.removeCallbacksAndMessages(null)
}
}
}
@SuppressLint("SwitchIntDef")
override fun onShuffleModeChanged(shuffleMode: Int) {
super.onShuffleModeChanged(shuffleMode)
when (shuffleMode) {
PlaybackStateCompat.SHUFFLE_MODE_ALL -> randomButton.setBackgroundResource(R.drawable.shuffle_button_selector)
PlaybackStateCompat.SHUFFLE_MODE_NONE -> randomButton.setBackgroundResource(R.drawable.shuffle_not_button_selector)
}
}
@SuppressLint("SwitchIntDef")
override fun onRepeatModeChanged(repeatMode: Int) {
super.onRepeatModeChanged(repeatMode)
when (repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> loopButton.setBackgroundResource(R.drawable.repeat_all_button_selector)
PlaybackStateCompat.REPEAT_MODE_ONE -> loopButton.setBackgroundResource(R.drawable.repeat_this_button_selector)
PlaybackStateCompat.REPEAT_MODE_NONE -> loopButton.setBackgroundResource(R.drawable.repeat_not_button_selector)
}
}
override fun onSessionEvent(event: String?, extras: Bundle?) {
super.onSessionEvent(event, extras)
when (event) {
"PositionChanged" -> {
seekBar.isEnabled = true
if ((activity as MainActivity).mediaController.playbackState.state == PlaybackStateCompat.STATE_PLAYING)
startSeekBarHandler()
}
"Buffered" -> seekBar.secondaryProgress = extras!!.getInt("percent") * 10
}
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
super.onMetadataChanged(metadata)
if (metadata != null) { // New or same currentSong
title.text = metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)
artist.text = metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)
songDuration = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION).toInt()
duration.text = millisToString(songDuration)
albumArt.setImageBitmap(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART))
songUri = metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
Server.getPlaylist(this@PlayerFragment, "Favourites")
}
if ((activity as MainActivity).mediaController.playbackState?.state == PlaybackStateCompat.STATE_PLAYING) {
playButton.setBackgroundResource(R.drawable.pause_button_selector)
startSeekBarHandler()
} else {
playButton.setBackgroundResource(R.drawable.play_button_selector)
updateSeekBar()
}
// Updates loop button
when ((activity as MainActivity).mediaController.repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ONE -> {
loopButton.setBackgroundResource(R.drawable.repeat_this_button_selector)
}
PlaybackStateCompat.REPEAT_MODE_ALL -> {
loopButton.setBackgroundResource(R.drawable.repeat_all_button_selector)
}
else -> {
loopButton.setBackgroundResource(R.drawable.repeat_not_button_selector)
}
}
// Updates random button
if ((activity as MainActivity).mediaController.shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL) {
randomButton.setBackgroundResource(R.drawable.shuffle_button_selector)
} else {
randomButton.setBackgroundResource(R.drawable.shuffle_not_button_selector)
}
}
}
inner class GListener : GestureDetector.SimpleOnGestureListener() {
private val SWIPE_MAX_OFF_PATH = 250 // How much you can derail from a straight line when swiping
private val SWIPE_MIN_DISTANCE = 120 // How long must the swipe be
private val SWIPE_MIN_VELOCITY = 120 // How quick must the swipe be
override fun onShowPress(p0: MotionEvent?) {}
override fun onSingleTapUp(p0: MotionEvent?): Boolean {
return true
}
override fun onDown(p0: MotionEvent?): Boolean {
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velX: Float, velY: Float): Boolean {
// Check if user made a swipe-like motion
if (abs(e1!!.y - e2!!.y) <= SWIPE_MAX_OFF_PATH && abs(velX) > SWIPE_MIN_VELOCITY && abs(e1.x - e2.x) >= SWIPE_MIN_DISTANCE) {
// Right to left swipe
if (e1.x >= e2.x) {
(activity as MainActivity).mediaController.transportControls.skipToNext()
}
// Left to right swipe
else {
(activity as MainActivity).mediaController.transportControls.skipToPrevious()
}
return true
}
return false
}
override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
return true
}
override fun onLongPress(p0: MotionEvent?) {}
override fun onDoubleTap(e: MotionEvent?): Boolean {
favouriteOps()
return true
}
}
}

View file

@ -0,0 +1,117 @@
package com.apollon.fragments
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.SearchView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.apollon.*
import com.apollon.adapters.SongAdapter
import com.apollon.classes.Playlist
import com.apollon.classes.Song
import com.squareup.picasso.Picasso
class SongsFragment : Fragment(), TaskListener {
private lateinit var mView: View
private val songs: ArrayList<Song> = ArrayList()
lateinit var playlist: Playlist
lateinit var recyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mView = inflater.inflate(R.layout.songs, container, false)
playlist = arguments?.getSerializable("playlist") as Playlist
val playlistThumbnail = mView.findViewById<ImageView>(R.id.playlist_thumbnail)
val playlistToolbar = mView.findViewById<Toolbar>(R.id.playlist_toolbar)
val search = mView.findViewById<SearchView>(R.id.search)
recyclerView = mView.findViewById(R.id.recycler_view)
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(s: String?): Boolean {
return false
}
override fun onQueryTextChange(s: String?): Boolean {
(recyclerView.adapter as SongAdapter).filter.filter(s)
return false
}
})
when (playlist.img_url) {
"all" -> playlistThumbnail.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.all))
"genre" -> playlistThumbnail.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.genre))
"favourites" -> playlistThumbnail.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.favourites))
"playlist" -> playlistThumbnail.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.playlist))
"artist" -> playlistThumbnail.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.artist))
"album" -> playlistThumbnail.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.album))
else -> Picasso.get().load(playlist.img_url).into(playlistThumbnail)
}
(activity as MainActivity).setSupportActionBar(playlistToolbar)
if (playlist is Playlist.AllSongs)
playlistToolbar.title = context?.getString(R.string.all)
else
playlistToolbar.title = playlist.title
// Creates a Grid Layout Manager
recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
// Access the RecyclerView Adapter and load the data into it
recyclerView.adapter = SongAdapter(playlist.title, songs, requireContext(), this)
// Loads elements into the ArrayList
addSongs(playlist)
return mView
}
// Adds songs to the empty ArrayList
private fun addSongs(playlist: Playlist) {
val uri = playlist.id
songs.clear()
when (playlist) {
is Playlist.AllSongs -> Server.getSongs(this)
is Playlist.Album -> Server.getAlbum(this, uri)
is Playlist.Custom -> Server.getPlaylist(this, uri)
is Playlist.Favourites -> Server.getPlaylist(this, uri)
else -> {
assert(false); return
}
}
}
override fun onTaskCompleted(result: TaskResult) {
if (result is TaskResult.ServerSongsResult) {
if (result.error == "") {
songs.clear()
result.result?.forEach { songs.add(it) }
// Access the RecyclerView Adapter and load the data into it
activity?.runOnUiThread {
(recyclerView.adapter as SongAdapter).songs = songs
(recyclerView.adapter as SongAdapter).notifyDataSetChanged()
}
} else
activity?.runOnUiThread {
Toast.makeText(context, result.error, Toast.LENGTH_LONG).show()
}
}
}
companion object {
fun newInstance(playlist: Playlist): SongsFragment {
val args = Bundle()
args.putSerializable("playlist", playlist)
val fragment = SongsFragment()
fragment.arguments = args
return fragment
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0"/>
<item
android:color="#00000000"
android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/mq_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/mq"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/mq"/>
<item android:state_pressed="false" android:drawable="@drawable/mq_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/back_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/back"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/delete_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/delete"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/edit_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/edit"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/favourite_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/favourite"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/favourite"/>
<item android:state_pressed="false" android:drawable="@drawable/favourite_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/favourite_not_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/favourite_not"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/favourite"/>
<item android:state_pressed="false" android:drawable="@drawable/favourite_not_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/forward_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/forward"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:angle="0"
android:startColor="@color/black"
android:centerColor="#00000000"
android:endColor="@color/black"
android:type="linear" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/hq_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/hq"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/hq"/>
<item android:state_pressed="false" android:drawable="@drawable/hq_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/lq_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/lq"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/lq"/>
<item android:state_pressed="false" android:drawable="@drawable/lq_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/lyrics_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/lyrics"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/lyrics"/>
<item android:state_pressed="false" android:drawable="@drawable/lyrics_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/pause_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/pause"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/play_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/play"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/plus_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/plus"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/repeat_all_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/repeat_all"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/repeat_not_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/repeat_not"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/repeat_this_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/repeat_this"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/share_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/share"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/share"/>
<item android:state_pressed="false" android:drawable="@drawable/share_pressed"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/shuffle_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/shuffle"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/shuffle_not_pressed"/>
<item android:state_pressed="false" android:drawable="@drawable/shuffle_not"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Some files were not shown because too many files have changed in this diff Show more