From 2529ddf956f62802d8cc71e6daba9b28d07e060c Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Sat, 9 Nov 2019 13:37:43 +0100 Subject: [PATCH] Initial commit --- .gitignore | 223 ++++++++++++++ .idea/codeStyles/Project.xml | 125 ++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/encodings.xml | 6 + .idea/misc.xml | 9 + .idea/render.experimental.xml | 6 + .idea/runConfigurations.xml | 12 + LICENSE.txt | 13 + app/build.gradle | 40 +++ app/proguard-rules.pro | 21 ++ .../zoo/wbooster/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 30 ++ .../java/zoo/wbooster/FullscreenActivity.kt | 124 ++++++++ app/src/main/java/zoo/wbooster/Services.kt | 275 ++++++++++++++++++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++++ .../res/drawable/ic_launcher_foreground.xml | 34 +++ .../main/res/layout/activity_fullscreen.xml | 74 +++++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 11852 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 5521 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 20172 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 42895 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 72186 bytes app/src/main/res/values-fr/strings.xml | 5 + app/src/main/res/values/attrs.xml | 12 + app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/styles.xml | 23 ++ .../test/java/zoo/wbooster/ExampleUnitTest.kt | 17 ++ build.gradle | 27 ++ gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++++++ gradlew.bat | 84 ++++++ settings.gradle | 2 + 35 files changed, 1573 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/render.experimental.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 LICENSE.txt create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/zoo/wbooster/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/zoo/wbooster/FullscreenActivity.kt create mode 100644 app/src/main/java/zoo/wbooster/Services.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_fullscreen.xml create mode 100755 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/zoo/wbooster/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d3d4a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,223 @@ +# Created by .ignore support plugin (hsz.mobi) +### Android template +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Gradle template +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Macos Sthit +*.DS_Store \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..45b5654 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..39c65f6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100644 index 0000000..8ec256a --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..fbbbe09 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ + LICENCE PUBLIQUE RIEN À BRANLER + Version 1, mars 2009 + +Copyright (C) 2019 BARTUCCIO Antoine + +La copie et la distribution de copies exactes de cette licence sont +autorisées, et toute modification est permise à condition de changer +le nom de la licence. + + CONDITIONS DE COPIE, DISTRIBUTON ET MODIFICATION + DE LA LICENCE PUBLIQUE RIEN À BRANLER + + 0. Faites ce que vous voulez, j’en ai RIEN À BRANLER. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..491e525 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "zoo.wbooster" + minSdkVersion 24 + 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.legacy:legacy-support-v4:1.0.0' + implementation 'com.github.yausername.youtubedl-android:library:0.6.+' + implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.6.+' + implementation 'com.beust:klaxon:5.0.1' + implementation 'com.vdurmont:emoji-java:5.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 diff --git a/app/src/androidTest/java/zoo/wbooster/ExampleInstrumentedTest.kt b/app/src/androidTest/java/zoo/wbooster/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9a97712 --- /dev/null +++ b/app/src/androidTest/java/zoo/wbooster/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package zoo.wbooster + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("zoo.wbooster", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e9bf1af --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/zoo/wbooster/FullscreenActivity.kt b/app/src/main/java/zoo/wbooster/FullscreenActivity.kt new file mode 100644 index 0000000..7d62d45 --- /dev/null +++ b/app/src/main/java/zoo/wbooster/FullscreenActivity.kt @@ -0,0 +1,124 @@ +package zoo.wbooster + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.net.Uri +import android.os.Build +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.os.IBinder +import android.view.View +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import kotlinx.android.synthetic.main.activity_fullscreen.* +import java.net.URI + + +/** + * Full screen activity devoted to play music + */ +class FullscreenActivity : AppCompatActivity() { + + private lateinit var mediaPlayerService: MediaPlayerService + private lateinit var downloaderService: DownloaderService + private var isDownloaderServiceBound: Boolean = false + private var isMediaPlayerServiceBound: Boolean = false + + + private val downloaderConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder = service as DownloaderService.DownloaderBinder + downloaderService = binder.getService() + isDownloaderServiceBound = true + } + + override fun onServiceDisconnected(name: ComponentName?) { + isDownloaderServiceBound = false + } + } + + private val mediaPlayerConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder = service as MediaPlayerService.MediaPlayerBinder + mediaPlayerService = binder.getService() + isMediaPlayerServiceBound = true + } + + override fun onServiceDisconnected(name: ComponentName?) { + isMediaPlayerServiceBound = false + } + } + + override fun onStart() { + super.onStart() + ContextCompat.startForegroundService(this, Intent(this, MediaPlayerService::class.java)) + Intent(this, MediaPlayerService::class.java).also { + bindService(it, mediaPlayerConnection, Context.BIND_AUTO_CREATE) + } + Intent(this, DownloaderService::class.java).also { + bindService(it, downloaderConnection, Context.BIND_AUTO_CREATE) + } + + } + + override fun onStop() { + super.onStop() + unbindService(mediaPlayerConnection) + unbindService(downloaderConnection) + isMediaPlayerServiceBound = false + isDownloaderServiceBound = false + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Upgrade Youtube-dl +// YT.updateYoutubeDL(application) + + // Set fullscreen immersive mode + setContentView(R.layout.activity_fullscreen) + fullscreen_content.systemUiVisibility = + View.SYSTEM_UI_FLAG_LOW_PROFILE or + View.SYSTEM_UI_FLAG_FULLSCREEN or + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + + + fullscreen_content_controls.visibility = View.VISIBLE + } + + @RequiresApi(Build.VERSION_CODES.O) + fun startPlaylist(v: View){ + playButton.setOnClickListener{} // Remove listner + playButton.isEnabled = false + + if (!isMediaPlayerServiceBound || !isDownloaderServiceBound){ + println("Problem connecting to services") + println("Media player status: $isMediaPlayerServiceBound") + println("Downloader status: $isDownloaderServiceBound") + return + } + + // Setup title update lambda + mediaPlayerService.titleUpdater = { + runOnUiThread(Runnable { + currently_playing.text = " $it ".repeat(400) + currently_playing.isSelected = true + }) + } + // Setup image update lambda + mediaPlayerService.thumbnailUpdater = { + runOnUiThread(Runnable { + thumbnail_view.setImageURI(Uri.parse(it)) + }) + } + + + downloaderService.startDownloading(mediaPlayerService, downloadBar) + mediaPlayerService.startPlaying() + } +} diff --git a/app/src/main/java/zoo/wbooster/Services.kt b/app/src/main/java/zoo/wbooster/Services.kt new file mode 100644 index 0000000..355010c --- /dev/null +++ b/app/src/main/java/zoo/wbooster/Services.kt @@ -0,0 +1,275 @@ +package zoo.wbooster + +import android.app.* +import android.content.Intent +import android.media.MediaPlayer +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.util.Log +import android.widget.ProgressBar +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NotificationCompat +import com.beust.klaxon.Klaxon +import com.vdurmont.emoji.EmojiParser +import com.yausername.ffmpeg.FFmpeg +import com.yausername.youtubedl_android.YoutubeDL +import com.yausername.youtubedl_android.YoutubeDLException +import com.yausername.youtubedl_android.YoutubeDLRequest +import java.lang.Exception +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class VideoUsefulDetails( + val path: String, + val title: String, + val thumbnail: String +) + +class VideoInfo( + val _type: String, + val id: String, + val ie_key: String, + val title: String, + val url: String +) + +class PlaylistInfos( + val _type: String, + val extractor: String, + val extractor_key: String, + val id: String, + val title: String, + val uploader: String, + val uploader_id: String, + val uploader_url: String, + val webpage_url: String, + val webpage_url_basename: String, + val entries: Array +){ + fun getRandomVideo() = this.entries.get(Random().nextInt(this.entries.size)) +} + +class DownloaderService: Service() { + + private var playlist: PlaylistInfos? = null + private val binder = DownloaderBinder() + private var isStarted = false + + inner class DownloaderBinder: Binder() { + fun getService(): DownloaderService = this@DownloaderService + } + + private fun customInit(){ + if (isStarted) return + + // Initialize Youtube-dl + // Thanks to https://github.com/yausername/youtubedl-android + try { + YT.init(application) + } catch (e: YoutubeDLException) { + Log.e("failed to initialize youtubedl-android", e.localizedMessage) + } + try { + FFmpeg.getInstance().init(application) + } catch (e: Exception) { + Log.e("failed to initialize FFmpeg", e.localizedMessage) + } + + playlist = downloadPlaylistInfo() + isStarted = true + } + + private fun downloadPlaylistInfo(): PlaylistInfos?{ + val request = YoutubeDLRequest(PLAYLIST_URL) + request.setOption("--dump-single-json") + request.setOption("--flat-playlist") + try { + return Klaxon().parse(YT.execute(request).out) + } catch (e: Exception) { + println("Error downloading playlist infos") + println(e) + return null + } + } + + fun startDownloading(mediaPlayerService: MediaPlayerService, downloadBar: ProgressBar) { + + if (playlist == null){ + println("No fucking playlist info") + return + } + + Thread(Runnable { + while (true) { + val video = playlist?.getRandomVideo() + val url = video?.url ?: "" + val info = try { + YT.getInfo(url) + } catch(e: YoutubeDLException) { + println("Error getting infos for video, skipping") + println(e) + continue + } + + val title = EmojiParser.removeAllEmojis(info.title) + + if (title == null){ + println("Error with music title, skipping") + continue + } + + val pathTemplate = "${applicationContext.filesDir}/${title}--${video?.id}" + val filePath = "$pathTemplate.mp3" + val thumnailPath = "$pathTemplate.jpg" + + val request = YoutubeDLRequest(url) + request.setOption("-x") + request.setOption("--audio-format", "mp3") + request.setOption("--write-thumbnail") + request.setOption("--cache-dir", "${applicationContext.filesDir}/.cache") + request.setOption("-o", "$pathTemplate.%(ext)s") + + + + try { + YT.execute(request) { progress, etaInSeconds -> + println("$progress% (ETA $etaInSeconds seconds)") + downloadBar.progress = progress.toInt() + } + mediaPlayerService.addNextSong(VideoUsefulDetails(filePath, info.title, thumnailPath)) + } catch (e: Exception) { + println(e) + } + } + + }).start() + } + + override fun onBind(intent: Intent?): IBinder? { + customInit() + return binder + } + + companion object { + /** + * URL to the WBooster playlist + */ + private val PLAYLIST_URL = + "https://www.youtube.com/playlist?list=PLQC73oq6JtgyYNOeifrJXXzZ1-F0Kgmbg" + private val YT = YoutubeDL.getInstance() + } +} + +class MediaPlayerService: Service(), MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener { + + private lateinit var notificationManager: NotificationManager + private lateinit var notificationBuilder: NotificationCompat.Builder + private var playerThread: Thread? = null + private val binder = MediaPlayerBinder() + private var mMediaPlayer: MediaPlayer? = null + private var playlistQueue: Queue = ConcurrentLinkedQueue() + private var currentSong: VideoUsefulDetails? = null + var titleUpdater = {title: String -> } + var thumbnailUpdater = { path: String ->} + + + inner class MediaPlayerBinder: Binder() { + fun getService(): MediaPlayerService = this@MediaPlayerService + } + + fun addNextSong(details: VideoUsefulDetails) { + this.playlistQueue.add(details) + } + + /* Get next music path and update interface */ + private fun updateNextSong(){ + + /* poll next song */ + currentSong = playlistQueue.poll() + while (currentSong == null){ + Thread.sleep(1000) /* Wait for another element */ + currentSong = playlistQueue.poll() + } + + // Update notification + notificationBuilder.setContentText(currentSong!!.title) + val notification = notificationBuilder.build() + notification.flags = Notification.FLAG_ONGOING_EVENT + notificationManager.notify(1, notificationBuilder.build()) + + // Update title + titleUpdater(currentSong!!.title) + + // Update image + thumbnailUpdater(currentSong!!.thumbnail) + + } + + fun startPlaying() { + playerThread?.interrupt() + playerThread = Thread(Runnable { + updateNextSong() + /* launch media player */ + mMediaPlayer = MediaPlayer().apply { + setDataSource(currentSong!!.path) + setOnPreparedListener(this@MediaPlayerService) + setOnCompletionListener(this@MediaPlayerService) + prepareAsync() // prepare async to not block main thread + } + }) + /* Start media player thread */ + playerThread?.start() + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + // Create Notification channel + val serviceChannel = NotificationChannel( + CHANNEL_ID, + "Foreground music channel", + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(serviceChannel) + + val notificationIntent = Intent(this, AppCompatActivity::class.java) + val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) + notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getText(R.string.app_name)) + .setContentText("") + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentIntent(pendingIntent) + + startForeground(1, notificationBuilder.build()) + + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?) = binder + + /** Called when MediaPlayer is ready */ + override fun onPrepared(mediaPlayer: MediaPlayer) { + mediaPlayer.start() + } + + override fun onCompletion(mp: MediaPlayer?) { + updateNextSong() + mp?.reset() + mp?.setDataSource(currentSong!!.path) + mp?.prepareAsync() + } + + override fun onDestroy() { + super.onDestroy() + mMediaPlayer?.release() + playerThread?.interrupt() + stopSelf() + } + + companion object { + val CHANNEL_ID = "ForegroundMediaPlayer" + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_fullscreen.xml b/app/src/main/res/layout/activity_fullscreen.xml new file mode 100644 index 0000000..52cb07c --- /dev/null +++ b/app/src/main/res/layout/activity_fullscreen.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + +