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 |