Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+46
View File
@@ -0,0 +1,46 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.media2)
implementation(project(":ktx"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.
#
# 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
+5
View File
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.music">
/
</manifest>
@@ -0,0 +1,268 @@
package de.mm20.launcher2.music
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.os.Bundle
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import android.view.KeyEvent
import androidx.core.content.edit
import androidx.core.graphics.scale
import androidx.lifecycle.MutableLiveData
import androidx.media2.common.MediaItem
import androidx.media2.common.MediaMetadata
import androidx.media2.common.SessionPlayer
import androidx.media2.session.MediaController
import androidx.media2.session.SessionCommand
import androidx.media2.session.SessionCommandGroup
import androidx.media2.session.SessionResult
import kotlinx.coroutines.*
import java.io.File
import java.util.concurrent.Executors
class MusicRepository private constructor(val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
val playbackState = MutableLiveData<PlaybackState>()
val title = MutableLiveData<String?>()
val artist = MutableLiveData<String?>()
val album = MutableLiveData<String?>()
val albumArt = MutableLiveData<Bitmap?>()
private var lastPlayer: String? = null
set(value) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREFS_KEY_LAST_PLAYER, value)
}
field = value
}
private var lastToken: String? = null
fun setMediaSession(token: MediaSessionCompat.Token) {
if (token.toString() == lastToken.toString()) return
mediaController?.close()
mediaController = MediaController.Builder(context)
.setSessionCompatToken(token)
.setControllerCallback(Executors.newSingleThreadExecutor(), mediaSessionCallback)
.build()
lastToken = token.toString()
}
private var mediaController: MediaController? = null
set(value) {
if (value == null) {
playbackState.postValue(PlaybackState.Stopped)
}
field = value
}
private val mediaSessionCallback = object : MediaController.ControllerCallback() {
override fun onConnected(controller: MediaController, allowedCommands: SessionCommandGroup) {
super.onConnected(controller, allowedCommands)
updateMetadata()
updateState()
}
override fun onCurrentMediaItemChanged(controller: MediaController, item: MediaItem?) {
super.onCurrentMediaItemChanged(controller, item)
updateMetadata()
}
override fun onPlayerStateChanged(controller: MediaController, state: Int) {
super.onPlayerStateChanged(controller, state)
updateState()
}
override fun onPlaybackInfoChanged(controller: MediaController, info: MediaController.PlaybackInfo) {
super.onPlaybackInfoChanged(controller, info)
Log.d("MM20", "CurrentPosition" + controller.currentPosition.toString())
}
override fun onDisconnected(controller: MediaController) {
super.onDisconnected(controller)
mediaController = null
}
/*override fun onMetadataChanged(metadata: MediaController?) {
super.onMetadataChanged(metadata)
updateState()
}
override fun onSessionDestroyed() {
super.onSessionDestroyed()
mediaController = null
hasActiveSession.value = false
playbackState.value = PlaybackState.Stopped
}*/
}
private fun updateMetadata() {
val metadata = mediaController?.currentMediaItem?.metadata ?: return
val title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
?: metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
val artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
?: metadata.getString(MediaMetadata.METADATA_KEY_COMPOSER)
?: metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR)
?: metadata.getString(MediaMetadata.METADATA_KEY_WRITER)
val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM)
lastPlayer = mediaController?.connectedToken?.packageName ?: lastPlayer
this@MusicRepository.title.postValue(title)
this@MusicRepository.artist.postValue(artist)
this@MusicRepository.album.postValue(album)
scope.launch {
withContext(Dispatchers.IO) {
val albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREFS_KEY_ALBUM_ART, if (albumArt == null) "null" else "notnull")
}
if (albumArt == null) {
this@MusicRepository.albumArt.postValue(null)
return@withContext
}
val size = context.resources.getDimensionPixelSize(R.dimen.album_art_size)
val scaledBitmap = albumArt.scale(size, size)
val file = File(context.cacheDir, "album_art")
val outStream = file.outputStream()
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
outStream.close()
this@MusicRepository.albumArt.postValue(scaledBitmap)
}
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREFS_KEY_TITLE, title)
putString(PREFS_KEY_ARTIST, artist)
putString(PREFS_KEY_ALBUM, album)
}
}
private fun updateState() {
val playbackState = when (mediaController?.playerState) {
SessionPlayer.PLAYER_STATE_PLAYING -> PlaybackState.Playing
SessionPlayer.PLAYER_STATE_PAUSED -> PlaybackState.Paused
else -> PlaybackState.Stopped
}
this@MusicRepository.playbackState.postValue(playbackState)
}
val hasActiveSession: Boolean = mediaController?.isConnected != null
init {
loadLastPlaybackMetadata()
}
private fun loadLastPlaybackMetadata() {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
lastPlayer = prefs.getString(PREFS_KEY_LAST_PLAYER, null)
title.value = prefs.getString(PREFS_KEY_TITLE, null)
artist.value = prefs.getString(PREFS_KEY_ARTIST, null)
album.value = prefs.getString(PREFS_KEY_ALBUM, null)
if (prefs.getString(PREFS_KEY_ALBUM_ART, "null") == "null") {
albumArt.value = null
} else scope.launch {
val albumArt = withContext(Dispatchers.IO) {
BitmapFactory.decodeFile(File(context.cacheDir, "album_art").absolutePath)
}
this@MusicRepository.albumArt.value = albumArt
}
playbackState.value = PlaybackState.Stopped
}
fun previous() {
if (mediaController?.skipToPreviousPlaylistItem()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
audioManager.dispatchMediaKeyEvent(downEvent)
val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
audioManager.dispatchMediaKeyEvent(upEvent)
}
}
fun next() {
if (mediaController?.skipToNextPlaylistItem()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)
audioManager.dispatchMediaKeyEvent(downEvent)
val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)
audioManager.dispatchMediaKeyEvent(upEvent)
}
}
fun play() {
if (mediaController?.play()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)
audioManager.dispatchMediaKeyEvent(downEvent)
val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)
audioManager.dispatchMediaKeyEvent(upEvent)
}
}
fun pause() {
if (mediaController?.pause()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)
audioManager.dispatchMediaKeyEvent(downEvent)
val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE)
audioManager.dispatchMediaKeyEvent(upEvent)
}
}
fun togglePause() {
if (playbackState.value != PlaybackState.Playing) play() else pause()
}
fun getLaunchIntent(context: Context): PendingIntent {
mediaController?.sessionActivity?.let {
return it
}
val intent = Intent(Intent.ACTION_MAIN)
.setPackage(lastPlayer)
.addCategory(Intent.CATEGORY_APP_MUSIC)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
}
fun openPlayerChooser(context: Context) {
context.startActivity(Intent.createChooser(
Intent(Intent.ACTION_MAIN)
.apply {
addCategory(Intent.CATEGORY_APP_MUSIC)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
},
null)
)
}
companion object {
private lateinit var instance: MusicRepository
fun getInstance(context: Context): MusicRepository {
if (!::instance.isInitialized) instance = MusicRepository(context.applicationContext)
return instance
}
private const val PREFS = "music"
private const val PREFS_KEY_TITLE = "title"
private const val PREFS_KEY_ARTIST = "artist"
private const val PREFS_KEY_ALBUM = "album"
private const val PREFS_KEY_ALBUM_ART = "album_art"
private const val PREFS_KEY_LAST_PLAYER = "last_player"
}
}
enum class PlaybackState {
Paused,
Playing,
Stopped
}
@@ -0,0 +1,50 @@
package de.mm20.launcher2.music
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
class MusicViewModel(app: Application) : AndroidViewModel(app) {
val musicRepository = MusicRepository.getInstance(app)
val title: LiveData<String?> = musicRepository.title
val artist: LiveData<String?> = musicRepository.artist
val album: LiveData<String?> = musicRepository.album
val albumArt: LiveData<Bitmap?> = musicRepository.albumArt
val playbackState: LiveData<PlaybackState> = musicRepository.playbackState
val hasActiveSession : Boolean = musicRepository.hasActiveSession
fun previous() {
musicRepository.previous()
}
fun next() {
musicRepository.next()
}
fun play() {
musicRepository.play()
}
fun pause() {
musicRepository.pause()
}
fun togglePause() {
musicRepository.togglePause()
}
fun getLaunchIntent(context: Context): PendingIntent {
return musicRepository.getLaunchIntent(context)
}
fun openPlayerChooser(context: Context) {
musicRepository.openPlayerChooser(context)
}
}
+3
View File
@@ -0,0 +1,3 @@
<resources>
<dimen name="album_art_size">144dp</dimen>
</resources>