progmobile
1
anno3/progmobile/apollon/app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
38
anno3/progmobile/apollon/app/build.gradle
Normal 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'
|
||||
}
|
21
anno3/progmobile/apollon/app/proguard-rules.pro
vendored
Normal 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
|
34
anno3/progmobile/apollon/app/src/main/AndroidManifest.xml
Normal 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>
|
BIN
anno3/progmobile/apollon/app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 38 KiB |
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.apollon
|
||||
|
||||
interface TaskListener {
|
||||
fun onTaskCompleted(result: TaskResult)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
|
@ -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, "")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 32 KiB |
|
@ -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>
|
BIN
anno3/progmobile/apollon/app/src/main/res/drawable-v24/logo.png
Normal file
After Width: | Height: | Size: 40 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 18 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/album.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/all.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/artist.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/back.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -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>
|
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/back_noti.png
Normal file
After Width: | Height: | Size: 638 B |
After Width: | Height: | Size: 29 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/delete.png
Normal file
After Width: | Height: | Size: 31 KiB |
|
@ -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>
|
After Width: | Height: | Size: 24 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/dots.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/edit.png
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -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>
|
After Width: | Height: | Size: 16 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/favourite.png
Normal file
After Width: | Height: | Size: 35 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 32 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 43 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/forward.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -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>
|
After Width: | Height: | Size: 525 B |
After Width: | Height: | Size: 29 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/genre.png
Normal file
After Width: | Height: | Size: 29 KiB |
|
@ -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>
|
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/hq.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 21 KiB |
|
@ -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>
|
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/icon.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/lq.png
Normal file
After Width: | Height: | Size: 20 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 21 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/lyrics.png
Normal file
After Width: | Height: | Size: 22 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 23 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/mq.png
Normal file
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 25 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/pause.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -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>
|
After Width: | Height: | Size: 193 B |
After Width: | Height: | Size: 24 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/play.png
Normal file
After Width: | Height: | Size: 23 KiB |
|
@ -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>
|
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/play_noti.png
Normal file
After Width: | Height: | Size: 405 B |
After Width: | Height: | Size: 31 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/plus.png
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -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>
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 32 KiB |
|
@ -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>
|
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 39 KiB |
|
@ -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>
|
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 33 KiB |
|
@ -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>
|
After Width: | Height: | Size: 41 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/share.png
Normal file
After Width: | Height: | Size: 37 KiB |
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 29 KiB |
BIN
anno3/progmobile/apollon/app/src/main/res/drawable/shuffle.png
Normal file
After Width: | Height: | Size: 26 KiB |
|
@ -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>
|
After Width: | Height: | Size: 30 KiB |
|
@ -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>
|
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 33 KiB |